Сдам Сам

ПОЛЕЗНОЕ


КАТЕГОРИИ







Вызов виртуальных методов, свойств и событий в CLR





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

Методы содержат код, выполняющий некоторые действия над типом (статические методы) или экземпляром типа (нестатические). У каждого метода есть имя, сигнатура и возвращаемое значение, которое может быть пустым (void). У типа может быть несколько методов с одним именем, но с разным числом параметров или разными возвращаемыми значениями. Можно определить и два метода с одним и тем же именем и параметрами, но с разным типом возвращаемого значения. Однако эта «возможность» большинством языков не используется (за исключением IL) — все они требуют, чтобы методы с одинаковым именем различались параметрами, а возвращаемое значение при определении уникальности метода игнорируется. Впрочем, при определении операторов преобразования язык С# смягчает это ограничение (см. главу 8 [11]).

Определим класс Employee с тремя различными видами методов.

internal class Employee

{

// Невиртуальный экземплярный метод

public Int32 GetYearsEmployed() {... }

// Виртуальный метод (виртуальный, значит, экземплярный)

public virtual String GenProgressReport() {... }

// Статический метод

public static Employee Lookup(String name) {... }

}

 

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

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

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

§ Инструкция callvirt используется только для вызова экземплярных и виртуальных методов. При вызове необходимо указать переменную, ссылающуюся на объект. Если с помощью этой инструкции вызывается невиртуальный метод экземпляра, тип переменной указывает, где определен необходимый метод. При использовании callvirt для вызова виртуального метода экземпляра CLR определяет настоящий тип объекта, на который ссылается переменная, и вызывает метод полиморфно. При компиляции такого вызова JIT-компилятор генерирует код для проверки значения переменной — если оно равно null, CLR сгенерирует исключение NullReferenceException. Из-за этой дополнительной проверки инструкция callvirt выполняется немного медленнее call. Проверка на null выполняется даже при вызове невиртуального метода экземпляра.

 

Давайте посмотрим, как эти инструкции используются в С#.

using System;

public sealed class Program

{

public static void Main()

{

Console.WriteLine(); // Вызов статического метода

Object obj = new Object();

obj.GetHashCode(); // Вызов виртуального метода экземпляра

obj.GetType(); // Вызов невиртуального метода экземпляра

}

}

 

Детальное рассмотрение IL-кода см. в главе 6 [11].

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

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

public class Set

{

private Int32 m_length = 0;

// Этот перегруженный метод - невиртуальный

public Int32 Find(Object value)

{ return Find(value, 0, m_length); }

// Этот перегруженный метод - невиртуальный

public Int32 Find(Object value, Int32 startlndex)

{ return Find(value, 0, m_length); }

/* Наиболее функциональный метод сделан виртуальным и может быть

переопределен*/

public virtual Int32 Find(Object value, Int32 startindex,

Int32 endindex)

{

// Здесь находится настоящая реализация, которую можно

// переопределить...

}

// Другие методы.

}

 

Разумное использование видимости типов и модификаторов доступа к членам – см. [11].

 


Константы и поля

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

Константы

Константа — это идентификатор, значение которого никогда не меняется. При определении идентификатора константы компилятор должен получить его значение во время компиляции. Затем компилятор сохраняет значение константы в метаданных модуля. Это значит, что константы можно определять только для таких типов, которые компилятор считает элементарными. В С# следующие типы считаются элементарными и могут быть использованы для определения констант: Boolean, Char, Byte, SByte, Intl6, UIntl6, Int32, UInt32, Int64, UInt64, Single, Double, Decimal и String.

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

Встретив в исходном тексте идентификатор константы, компилятор просматривает метаданные модуля, в котором она определена, извлекает значение константы и внедряет его в генерируемый им IL-код. Поскольку значение константы внедряется прямо в код, в период выполнения память для констант не выделяется. Кроме того, нельзя получать адрес константы и передавать ее по ссылке. Эти ограничения также означают, что изменять значения константы в разных версиях модуля нельзя, поэтому константу надо использовать, только когда точно известно, что ее значение никогда не изменится (хороший пример — определение константы MaxIntl6 со значением 32767). Поясню на примере, что я имею в виду.

Возьмем код и скомпилируем его в сборку DLL:

using System;

public sealed class SomeLibraryType

{

/* ПРИМЕЧАНИЕ: С# не позволяет использовать для констант

модификатор static, поскольку всегда подразумевается, что

константы являются статическими*/

public const Int32 MaxEntriesInList = 50;

}

 

Затем скомпонуем сборку приложения из такого кода:

using System;

public sealed class Program

{

public static void Main()

{

Console.WriteLine("Max entries supported in list: "

+ SomeLibraryType.MaxEntriesInList);

}

}

 

Нетрудно заметить, что код приложения содержит ссылку на константу MaxEntriesInList. При компоновке этого кода компилятор, обнаружив, что MaxEntriesInList — это литерал константы со значением 50, внедрит значение 50 с типом Int32 прямо в IL-код приложения. В сущности, после компоновки кода приложения сборка DLL даже не будет загружаться в период выполнения, поэтому ее можно просто удалить с диска.

Думаю, теперь проблема с управлением версиями при использовании констант должна стать очевидной. Если разработчик изменит значение константы MaxEntriesInList на 1000 и заново опубликует сборку DLL, это не повлияет на код самого приложения. Чтобы в приложении использовалось новое значение константы, его придется перекомпилировать. Нельзя применять константы, если модуль должен задействовать значение, определенное в другом модуле, во время выполнения (а не во время компиляции). В этом случае вместо констант следует использовать неизменяемые поля.

Поля

Поле (field) — это член данных, который хранит экземпляр размерного типа или ссылку на ссылочный тип. В табл. 6.1 приведены модификаторы, применяемые по отношению к полям.

Как видно из таблицы, общеязыковая среда (CLR) поддерживает поля как типов (статические), так и экземпляров (нестатические). Динамическая память для хранения поля типа выделяется в пределах объекта-типа, который создается при загрузке типа в домен AppDomain (см. главу 21[11]), что обычно происходит при JIT-компиляции любого метода, ссылающегося на этот тип. Динамическая память для хранения экземплярных полей выделяется при создании экземпляра данного типа.

Табл. 6.1. Модификаторы полей

Термин CLR Термин С# Описание
Static static Поле является частью состояния типа, а не объекта
Instance (default) Поле связано с экземпляром типа, а не самим типом
InitOnly readonly Запись в поле разрешается только из кода метода конструктора
Volatile volatile Код, обращающийся к полю, не обязательно специально должен оптимизироваться в отношении управления типами компилятором, CLR или оборудованием. Только следующие типы могут объявляться как volatile: все ссылочные типы, Single, Boolean, Byte, SByte, Intl6, UIntl6, Int32, UInt32, Char, а также все перечислимые типы, основанные на следующих типах: Byte, SByte, Intl6, Ulntl6, Int32 или UInt32

 

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

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

Попробуем решить проблему с управлением версиями в примере из раздела «Константы», используя статические неизменяемые поля. Вот новая версия кода сборки DLL:

using System;

public sealed class SomeLibraryType

{

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

public static readonly Int32 MaxEntriesInList = 50;

}

 

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

Допустим, разработчик сборки изменил значение поля с 50 на 1000 и скомпоновал сборку заново. При повторном исполнении код приложения автоматически задействует новое значение —1000. В этом случае не обязательно компоновать код приложения заново — он просто работает в том виде, в каком был (хотя и чуть медленнее). Но здесь есть подводный камень: этот сценарий предполагает, что у новой сборки нет строгого имени или что политика управления версиями приложения заставляет CLR загружать именно эту новую версию сборки.

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

public sealed class SomeType

{

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

сохраняется в памяти во время инициализации класса во время

выполнения*/

public static readonly Random s_random = new Random();

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

private static Int32 s_numberOfWrites = 0;

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

public readonly String Pathname = "Untitled";

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

private System.IO.FileStream m_fs;

public SomeType(String pathname)

{

/*Эта строка изменяет значение неизменяемого поля.В данном случае

это возможно, так как показанный ниже код расположен в

конструкторе*/

this.Pathname = pathname;

public String DoSomething()

{

/*Эта строка читает и записывает значение статического

изменяемого поля*/

s_numberOfWrites = s_numberOfWrites + 1;

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

return Pathname;

}

}

}

 

Многие поля в нашем примере инициализируются при объявлении (inline). C# позволяет использовать этот удобный синтаксис для инициализации констант, а также изменяемых и неизменяемых полей. Как я покажу в главе 8 [11], С# считает, что инициализация поля при объявлении — это краткий синтаксис, позволяющий инициализировать поле во время исполнения конструктора. Вместе с тем, в С# возможны проблемы с производительностью, которые нужно учитывать при инициализация поля с использованием синтаксиса встраивания, а не присвоения в конструкторе. Они также обсуждаются в главе 8 [11].

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

public sealed class AType

{

// InvalidChars должно всегда ссылаться на один объект массива

public static readonly Char[] InvalidChars = new Char[] {'А','В','С'};

}

public sealed class AnotherType

{

public static void M()

{

/* Следующие строки кода вполне корректны, компилируются

и успешно изменяют символы в массиве InvalidChars*/

AType.InvalidChars[O] = 'X';

AType.InvalidChars[1] = 'Y';

AType.InvalidChars[2] = 'Z';

/*Следующая строка некорректна и не скомпилируется,

так как то, на что ссылается InvalidChars, изменить нельзя*/

AType.InvalidChars = new Char[] {'Х’,’Y’,'Z’};

}

}








ЧТО ПРОИСХОДИТ ВО ВЗРОСЛОЙ ЖИЗНИ? Если вы все еще «неправильно» связаны с матерью, вы избегаете отделения и независимого взрослого существования...

ЧТО ТАКОЕ УВЕРЕННОЕ ПОВЕДЕНИЕ В МЕЖЛИЧНОСТНЫХ ОТНОШЕНИЯХ? Исторически существует три основных модели различий, существующих между...

Что способствует осуществлению желаний? Стопроцентная, непоколебимая уверенность в своем...

Система охраняемых территорий в США Изучение особо охраняемых природных территорий(ООПТ) США представляет особый интерес по многим причинам...





Не нашли то, что искали? Воспользуйтесь поиском гугл на сайте:


©2015- 2024 zdamsam.ru Размещенные материалы защищены законодательством РФ.