□ В алгоритме должно использоваться как минимум одно экземплярное поле.
□ Поля, используемые в алгоритме, в идеале не должны изменяться, то есть они должны инициализироваться при создании объекта и сохранять значение в течение всей его жизни.
□ Алгоритм должен быть максимально быстрым.
□ Объекты с одинаковым значением должны возвращать одинаковые коды. Например, два объекта String, содержащие одинаковый текст, должны возвращать одно значение хеш-кода.
Реализация GetHashCode в System. Object ничего «не знает» о производных типах и их полях. Поэтому этот метод возвращает число, однозначно идентифицирующее объект в пределах домена приложений; при этом гарантируется, что это число не изменится на протяжении всей жизни объекта.
ВНИМАНИЕ
Если вы взялись за собственную реализацию хеш-таблиц или пишете код, в котором будет вызываться метод GetHashCode, никогда не сохраняйте значения хеш-кодов. Они подвержены изменениям в силу своей природы. Например, при переходе к следующей версии типа алгоритм вычисления хеш-кода объекта может просто измениться.
Я знаю компанию, которая проигнорировала это предупреждение. Посетители ее веб-сайта создавали новые учетные записи, выбирая имя пользователя и пароль. Строка (String) пароля передавалась методу GetHashCode, а полученный хеш- код сохранялся в базе данных. В дальнейшем при входе на веб-сайт посетители указывали свой пароль, который снова обрабатывался методом GetHashCode, и полученный хеш-код сравнивался с сохраненным в базе данных. При совпадении пользователю предоставлялся доступ. К несчастью, после обновления версии CLR метод GetHashCode типа String изменился и стал возвращать другой хеш-код. Результат оказался плачевным — все пользователи потеряли доступ к веб-сайту!
Примитивный тип данных dynamic
Язык C# обеспечивает безопасность типов данных. Это означает, что все выражения разрешаются в экземпляр типа и компилятор генерирует только тот код, который старается представить операции, правомерные для данного типа данных. Преимущество от использования языка, обеспечивающего безопасность типов данных, заключается в том, что еще на этапе компиляции обнаруживается множество ошибок программирования, что помогает программисту скорректировать код перед его выполнением. К тому же при помощи подобных языков программирования можно получать более быстрые приложения, потому что они разрешают больше допущений еще на этапе компиляции и затем переводят эти допущения в язык IL или метаданные.
Однако возможны неприятные ситуации, возникающие из-за того, что программа должна выполняться на основе информации, недоступной до ее выполнения. Если вы используете языки программирования, обеспечивающие безопасность данных (например, С#) для взаимодействия с этой информацией, синтаксис становится громоздким, особенно в случае;, если вы работаете с множеством строк, в результате производительность приложения падает. Если вы пишете приложение на «чистом» языке С#, неприятная ситуация может подстерегать вас только во время работы с информацией, определяемой на этапе выполнения, когда вы используете отражения (см. главу 23). Однако многие разработчики используют также C# для связи с компонентами, не реализованными на С#. Некоторые из этих компонентов могут быть написаны на динамических языках, например Python или Ruby, или быть COM-объектами, которые поддерживают интерфейс IDispatch (возможно, реализованный на С или C++), или объектами модели DOM (Document Object Model), реализованными при помощи разных языков и технологий. Взаимодействие с DOM-объектами особенно полезно для построения Silverlight-приложений.
Для того чтобы облегчить разработку при помощи отражений или коммуникаций с другими компонентами, компилятор C# предлагает помечать типы как динамические (dynamic). Вы также можете записывать результаты вычисления выражений в переменную и пометить ее тип как динамический. Затем динамическое выражение (переменная) может быть использовано для вызовов членов класса, например поля, свойства/индексатора, метода, делегата, или унарных/бинарных операторов. Когда ваш код вызывает член класса при помощи динамического выражения (переменной), компилятор создает специальный IL-код, который описывает желаемую операцию. Этот код называется полезной нагрузкой (payload). Во время выполнения программы он определяет существующую операцию для выполнения на основе действительного типа объекта, на который ссылается динамическое выражение (переменная).
Следующий код поясняет, о чем идет речь:
internal static class DynamicDemo {
public static void Main() { dynamic value;
for (Int32 demo = 0; demo < 2; demo++) {
value = (demo == 0) ? (dynamic) 5 : (dynamic) "A"; value = value + value;
M(value);
}
}
private static void M(Int32 n) { Console.Writel_ine("M(Int32) : " + n); }
private static void M(String s) { Console.Writel_ine( "M(String): " + s); }
}
После выполнения метода Main получается следующий результат:
M(Int32): 10 M(String): АА
Для того чтобы понять, что здесь происходит, обратимся к оператору +. У этого оператора имеются операнды типа с пометкой dynamic. По этой причине компилятор C# генерирует код полезной нагрузки, который проверяет действительный тип переменной value во время выполнения и определяет, что должен делать оператор +.
Во время первого вызова оператора + значение его аргумента равно 5 (тип Int32), поэтому результат равен 10 (тоже тип Int32). Результат присваивается переменной value. Затем вызывается метод М, которому передается value. Для вызова метода М компилятор создает код полезной нагрузки, который на этапе выполнения будет проверять действительный тип значения переменной, переданной методу М. Когда value содержит тип Int32, вызывается перегрузка метода М, получающая параметр Int32.
Во время второго вызова + значение его аргумента равно А (тип String), а результат представляет собой строку АА (результат конкатенации А с собой). Затем снова вызывается метод М, которому передается value. На этот раз код полезной нагрузки определяет, что действительный тип, переданный в М, является строковым, и вызывает перегруженную версию М со строковым параметром.
Когда тип поля, параметр метода, возвращаемый тип метода или локальная переменная снабжаются пометкой dynamic, компилятор конвертирует этот тип в тип System. Ob ject и применяет экземпляр System. Runtime. CompilerServices. DynamicAttribute к полю, параметру или возвращаемому типу в метаданных. Если локальная переменная определена как динамическая, то тип переменной также будет типом Object, но атрибут DynamicAttribute неприменим к локальным переменным из-за того, что они используются только внутри метода. Из-за того, что типы dynamic и Object одинаковы, вы не сможете создавать методы с сигнатурами, отличающимися только типами dynamic и Object.
Тип dynamic можно использовать для определения аргументов типов обобщенных классов (ссылочный тип), структур (значимый тип), интерфейсов, делегатов или методов. Когда вы это делаете, компилятор конвертирует тип dynamic в Object и применяет DynamicAttribute к различным частям метаданных, где это необходимо. Обратите внимание, что обобщенный код, который вы используете, уже скомпилирован в соответствии с типом Object, и динамическая отправка не осуществляется, поскольку компилятор не производит код полезной нагрузки в обобщенном коде.
Любое выражение может быть явно приведено к dynamic, поскольку все выражения дают в результате тип, производный от Object[7]. В общем случае компилятор не позволит вам написать код с неявным приведением выражения от типа Object к другому типу, вы должны использовать явное приведение типов. Однако компилятор разрешит выполнить приведение типа dynamic к другому типу с использованием синтаксиса неявного приведения.
Object ol = 123; // ОК: Неявное приведение Int32 к Object (упаковка)
Int32 nl = ol; // Ошибка: Нет неявного приведения Object к Int32
Int32 n2 = (Int32) ol; // ОК: Явное приведение Object к Int32 (распаковка)
dynamic dl = 123; // OK: Неявное приведение Int32 к dynamic (упаковка)
Int32 пЗ = d; // OK: Неявное приведение dynamic к Int32 (распаковка)
Пока компилятор позволяет пренебрегать явным приведением динамического типа к другому типу данных, среда CLR на этапе выполнения проверяет правильность приведения с целью обеспечения безопасности типов. Если тип объекта несовместим с приведением, CLR выдает исключение InvalidCastException. Обратите внимание на следующий код:
dynamic d = 123;
var result = M(d); // 'var result' - то же, что 'dynamic result'
Здесь компилятор позволяет коду компилироваться, потому что на этапе компиляции он не знает, какой из методов М будет вызван. Следовательно, он также не знает, какой тип будет возвращен методом М. Компилятор предполагает, что переменная result имеет динамический тип. Вы можете убедиться в этом, когда наведете указатель мыши на переменную var в редакторе Visual Studio — во всплывающем IntelliSense-окне вы увидите следующее.
dynamic: Represents an object whose operations will be resolved at runtime.
Если метод M, вызванный на этапе выполнения, возвращает void, выдается исключение Microsoft.CSharp.RuntimeBinder.RuntimeBinderException.
ВНИМАНИЕ
He путайте типы dynamic и var. Объявление локальной переменной как var является синтаксическим указанием компилятору подставлять специальные данные из соответствующего выражения. Ключевое слово var может использоваться только для объявления локальных переменных внутри метода, тогда как ключевое слово dynamic может указываться с локальными переменными, полями и аргументами. Вы не можете привести выражение ктипу var, но вы можете привести его к типу dynamic. Вы должны явно инициализировать переменную, объявленную как var, тогда как переменную, объявленную как dynamic, инициализировать нельзя. Больше подробно о типе var рассказывается в главе 9.
При преобразовании типа dynamic в другой статический тип результатом будет, очевидно, тоже статический тип. Аналогичным образом при создании типа с передачей конструктору одного и более аргументов dynamic результатом будет объект того типа, который вы создаете:
dynamic d = 123;
var х = (Int32) d; // Конвертация: 'var x' одинаково c 'Int32 x'
var dt = new DateTime(d); // Создание: 'var dt' одинаково c 'DateTime dt'
Если выражение dynamic задается как коллекция в инструкции foreach или как ресурс в директиве using, то компилятор генерирует код, который попытается привести выражение к необобщенному интерфейсу System. IEnumerable или интерфейсу System.IDisposable соответственно. Если приведение типов выполняется успешно, то выражение используется, а код выполняется нормально. В противном случае будет выдано исключение Microsoft .CSharp. RuntimeBinder. RuntimeBinderException.
ВНИМАНИЕ
Выражение dynamic реально имееттотже тип, что и System.Object. Компилятор принимает операции с выражением какдопустимые и не генерирует ни предупреждений, ни ошибок. Однако исключения могут быть выданы на этапе выполнения программы, если программа попытается выполнить недопустимую операцию. К тому же Visual Studio не предоставляет какой-либо lntelliSense-поддержки для написания кода с динамическими выражениями. Вы не можете определить метод расширения для dynamic (об этом рассказывается в главе 8), хотя можете его определить для Object. И вы можете использовать лямбда-выражение или анонимный метод (они оба обсуждаются в главе 17) в качестве аргумента при вызове динамического метода, потому что компилятор не может вычислить фактически используемые типы.
Рассмотрим пример кода на C# с использованием COM-объекта IDispatch для создания книги Microsoft Office Excel и размещения строки в ячейке А1.
using Microsoft.Office.Interop.Excel;
public static void Main() {
Application excel = new ApplicationQ;
excel.Visible = true;
excel.Workbooks.Add(Type.Missing);
((Range)excel.Cells[l, 1]).Value = "Text in cell Al";
// Помещаем эту строку в ячейку А1.
}
Без типа dynamic значение, возвращаемое excel. Cells [1,1], имеет тип Object, который должен быть приведен к типу Range перед обращением к его свойству Value. Однако во время генерации выполняемой «обертки» для СОМ-объекта любое использование типа VARIANT в COM-методе будет преобразовано в dynamic. Следовательно, поскольку выражение excel. Cells [1, 1] относится к типу dynamic, вам не обязательно явно приводить его к типу Range для обращения к свойству Value. Преобразование к dynamic значительно упрощает код, взаимодействующий с COM-объектами. Пример более простого кода:
using Microsoft.Office.Interop. Excel;
public static void Main() {
Application excel = new ApplicationQ;
excel.Visible = true;
excel.Workbooks.Add(Type.Missing);
excel.Cells[l, 1].Value = "Text in cell Al";
// Помещаем эту строку в ячейку Al
}
Следующий фрагмент показывает, как использовать отражение для вызова метода (Contains) с аргументом типа String ("ff") для строки (" Jeffrey Richter") и поместить результат с типом Int32 в локальную переменную result.
Object target = "leffrey Richter";
Object arg = "ff";
// Находим метод, который подходит по типам аргументов Туре[] argTypes = newType[] { arg.GetType() };
Methodlnfo method = target.GetType().GetMethod("Contains", argTypes);
// Вызываем метод с желаемым аргументом Object[] arguments = newObjectf] { arg };
Boolean result = Convert.ToBoolean(method.Invoke(target, arguments));
Если использовать тип C# dynamic, этот код можно значительно улучшить с точки зрения синтаксиса.
dynamic target = "leffrey Richter"; dynamic arg = "ft";
Boolean result = target.Contains(arg);
Ранее я уже говорил о том, что компилятор C# на этапе выполнения программы генерирует код полезной нагрузки, основываясь на действительных типах объекта. Этот код полезной нагрузки использует класс, известный как компоновщик (runtime binder). Различные языки программирования определяют собственных компоновщиков, инкапсулируя в них правила языка. Код для компоновщика C# находится в сборке Microsoft. CSharp. dll, поэтому ссылка на эту сборку должна включаться в любой проект, использующий ключевое слово dynamic. Эта сборка ссылается на файл параметров по умолчанию, CSC.rsp. Код из этой сборки знает, что при применении оператора + применяется к двум объектам типа Int32 следует генерировать код сложения, а для двух объектов типа String — код конкатенации.
Во время выполнения сборка Microsoft.CSharp.dll должна быть загружена в домен приложений, что снизит производительность приложения и повысит расход памяти. Кроме того, сборка Microsoft.SCharp.dll загружает библиотеки System.dll и System.Core.dll. А если вы используете тип dynamic для связи с СОМ-объектами, загружается и библиотека System.Dynamic.dll. И когда будет выполнен код полезной нагрузки, генерирующий динамический код во время выполнения, этот код окажется в сборке, названной анонимной сборкой динамических методов (Anonymously Hosted Dynamic Methods Assembly). Назначение этого кода заключается в повышении производительности динамических ссылок в ситуациях, в которых конкретное место вызова (call site) выдает много вызовов с динамическими аргументами, соответствующих одному типу на этапе выполнения.
Из-за всех издержек, связанных с особенностями встроенных динамических вычислений в С#, вы должны осознанно решить, что именно вы желаете добиться от динамического кода: превосходной производительности приложения при загрузке всех этих сборок или оптимального расходования памяти. Если динамический код используется только в паре мест вашего программного кода, разумнее придерживаться старого подхода: либо вызывать методы отражения (для управляемых объектов), либо «вручную» приводить типы (для СОМ-объектов).
Во время выполнения компоновщик C# разрешает динамические операции в соответствии с типом объекта. Сначала компоновщик проверяет, реализуется
ли типом интерфейс IDynamicMetObjectProvider. И если интерфейс реализован, вызывается метод GetMetaObject, который возвращает тип, производный от DynamicMetaObject. Этот тип может обработать все привязки членов, методов и операторов, связанные с объектом. Интерфейс IDynamicMetaObjectProvider и основной класс DynamicMetaObject определены в пространстве имен System. Dynamic и находятся в сборке System.Core.dll.
Динамические языки, такие как Python и Ruby, используют типы, производные от DynamicMetaObject, что позволяет взаимодействовать с ними из других языков (например, С#). Аналогичным образом компоновщик C# при связи с СОМ- компонентами будет использовать порожденный тип DynamicMetaObject, умеющий взаимодействовать с COM-компонентами. Порожденный тип DynamicMetaObject определен в сборке System.Dynamic.dll.
Если тип объекта, используемый в динамическом выражении, не реализует интерфейс IDynamicMetaOb jectProvider, тогда компилятор C# воспринимает его как обычный объект типа языка C# и все связанные с ним действия осуществляет через отражение.
Одно из ограничений динамических типов заключается в том, что они могут использоваться только для обращения к членам экземпляров, потому что динамическая переменная должна ссылаться на объект. Однако в некоторых случаях бывает полезно динамически вызывать статические методы типа, определяемого во время выполнения. Для этого я создал класс StaticMemberDynamicWrapper, производный от класса System. Dynamic. DynamicObject, реализующего интерфейс IDynamicMetaObjectProvider. Во внутренней реализации этого класса активно используется отражение (см. главу 23). Ниже приведен код моего класса StaticMemberDynamicWrapper.
internal sealed class StaticMemberDynamicWrapper : DynamicObject {
private readonly Typelnfo mtype;
public StaticMemberDynamicWrapper(Type type) { mtype = type.GetTypeInfo(); }
public override IEnumerable<String> GetDynamicMemberNames() { return m_type.DeclaredMembers.Select(mi => mi.Name);
}
public override Boolean TryGetMember(GetMemberBinder binder, out object result)
{
result = null;
var field = FindField(binder.Name);
if (field != null) { result = field.GetValue(null); return true; } var prop = FindProperty(binder.Name, true);
if (prop != null) { result = prop.GetValue(null, null); return true; } return false;
}
public override Boolean TrySetMember(SetMemberBinder binder, object value) { var field = FindField(binder.Name);
if (field != null) { field.SetValue(null, value); return true; } var prop = FindProperty(binder.Name, false);
if (prop != null) { prop.SetValue(null, value, null); return true; } return false;
public override Boolean TryInvokeMember(InvokeMemberBinder binder, ObJect[] args,
out Object result) {
Methodlnfo method = FindMethod(binder.Name); if (method == null) { result = null; return false; } result = method.Invoke(null, args); return true;
private Methodlnfo FindMethod(String name, Type[] paramTypes) {
return m_type.DeclaredMethods.FirstOrDefault(mi => mi.IsPublic && mi.IsStatic
&& mi.Name == name
&& ParametersMatch(mi.GetParametersQ, paramTypes));
}
private Boolean ParametersMatch(ParameterInfo[] parameters, Type[] paramTypes) {
if (parameters.Length != paramTypes.Length) return false; for (Int32 i = 0; i < parameters.Length; i++)
if (parameters[i].ParameterType != paramTypes[i]) return false; return true;
}
private Fieldlnfo FindField(String name) {
return m_type.DeclaredFields.FirstOrDefault(fi => fi.IsPublic && fi.IsStatic && fi.Name == name);
}
private Propertylnfo FindProperty(String name, Boolean get) { if (get)
return mtype.DeclaredProperties.FirstOrDefault( pi => pi.Name == name && pi.GetMethod != null && pi.GetMethod.IsPublic && pi.GetMethod.IsStatic);
return mtype.DeclaredProperties.FirstOrDefault( pi => pi.Name == name && pi.SetMethod != null && pi.SetMethod.IsPublic && pi.SetMethod.IsStatic);
>
>
Чтобы вызвать статический метод динамически, сконструируйте экземпляр класса с передачей Туре и сохраните ссылку на него в динамическую переменную. Затем вызовите нужный статический метод с использованием синтаксиса вызова экземплярного метода. Пример вызова статического метода Concat(String, String) класса String:
dynamic stringType = new StaticMemberDynamicl/drapper(typeof(String)); var r = stringType.Concat("A", "В""); // Динамический вызов статического
// метода Concat класса String Console.WriteLine(r); // выводится "AB"
Глава 6. Основные сведения о членах и типах
В главах 4 и 5 были рассмотрены типы и операции, применимые ко всем экземплярам любого типа. Кроме того, объяснялось, почему все типы делятся на две категории — ссылочные и значимые. В этой и последующих главах я показываю, как проектировать типы с использованием различных членов, которые можно определить в типах. В главах с 7 по 11 эти члены рассматриваются подробнее.
Члены типа
В типе можно определить следующие члены.
□ Константа — идентификатор, определяющий некую постоянную величину. Эти идентификаторы обычно используют, чтобы упростить чтение кода, а также для удобства сопровождения и поддержки. Константы всегда связаны с типом, а не с экземпляром типа, а на логическом уровне константы всегда являются статическими членами. Подробнее о константах см. главу 7.
□ Поле представляет собой значение данных, доступное только для чтения или для чтения/записи. Поле может быть статическим — тогда оно является частью состояния типа. Поле может быть экземплярным (нестатическим) — тогда оно является частью состояния конкретного объекта. Я настоятельно рекомендую ограничивать доступ к полям, чтобы внешний код не мог нарушить состояние типа или объекта. Подробнее о полях см. главу 7.
□ Конструктор экземпляров — метод, служащий для инициализации полей экземпляра при его создании. Подробнее о конструкторах экземпляров см. главу 8.
□ Конструктор типа — метод, используемый для инициализации статических полей типа. Подробнее о конструкторах типа см. главу 8.
□ Метод представляет собой функцию, выполняющую операции, которые изменяют или запрашивают состояние типа (статический метод) или объекта (экземплярный метод). Методы обычно осуществляют чтение и запись полей типов или объектов. Подробнее о методах см. главу 8.
□ Перегруженный оператор определяет, что нужно проделать с объектом при применении к нему конкретного оператора. Перегрузка операторов не входит в общеязыковую спецификацию CLS, поскольку не все языки программирования ее поддерживают. Подробнее о перегруженных операторах см. главу 8.
□ Оператор преобразования — метод, задающий порядок явного или неявного преобразования объекта из одного типа в другой. Операторы преобразования не входят в спецификацию CLS по той же причине, что и перегруженные операторы. Подробнее об операторах преобразования см. главу 8.
□ Свойство представляет собой механизм, позволяющий применить простой синтаксис (напоминающий обращение к полям) для установки или получения части логического состояния типа или объекта с контролем логической целостности этого состояния. Свойства бывают необобщенными (распространенный случай) и обобщенными (встречаются редко, в основном в классах коллекций). Подробнее о свойствах см. главу 10.
□ Событие — механизм статических событий позволяет типу отправлять уведомления статическим или экземплярным методам. Механизм экземплярных (нестатических) событий позволяет объекту посылать уведомление статическому или экземплярному методу. События обычно инициируются в ответ на изменение состояния типа или объекта, порождающего событие. Событие состоит из двух методов, позволяющих статическим или экземплярным методам регистрировать и отменять регистрацию (подписку) на событие. Помимо этих двух методов, в событиях обычно используется поле-делегат для управления набором зарегистрированных методов. Подробнее о событиях см. главу 11.
□ Тип позволяет определять другие вложенные в него типы. Обычно этот подход применяется для разбиения большого, сложного типа на небольшие блоки с целью упростить его реализацию.
Еще раз подчеркну, что цель данной главы состоит не в подробном описании различных членов, а в изложении общих принципов и объяснении сходных аспектов этих членов.
Независимо от используемого языка программирования, компилятор должен обработать исходный код и создать метаданные и IL-код для всех членов типа. Формат метаданных един и не зависит от выбранного языка программирования — именно поэтому CLR называют общеязыковой исполняющей средой. Метаданные — это стандартная информация, которую предоставляют и используют все языки, позволяя коду на одном языке программирования без проблем обращаться к коду на совершенно другом языке.
Стандартный формат метаданных также используется средой CLR для определения порядка поведения констант, полей, конструкторов, методов, свойств и событий во время выполнения. Короче говоря, метаданные — это ключ ко всей платформе разработки Microsoft .NET Framework; они обеспечивают интеграцию языков, типов и объектов.
В следующем примере на C# показано определение типа со всеми возможными членами. Этот код успешно компилируется (не без предупреждений), но пользы от него немного — он всего лишь демонстрирует, как компилятор транслирует такой тип и его члены в метаданные. Еще раз напомню, что каждый из членов в отдельности детально рассмотрен в следующих главах.
using System;
public sealed class SomeType { //1
// Вложенный класс
private class SomeNestedType { } //2
// Константа, неизменяемое и статическое изменяемое поле // Constant, readonly, and static read/write field private const Int32 c_SomeConstant =1 ; //3
private readonly String mSomeReadOnlyField = "2"; // 4
private static Int32 sSomeReadWriteField =3; //5
// Конструктор типа
static SomeTypeQ { } //6
// Конструкторы экземпляров
public SomeType(Int32 x) { } //7
public SomeTypeQ { } //8
// Экземплярный и статический методы
private String InstanceMethodQ { return null; } //9
public static void MainQ {} // 10
// Необобщенное экземплярное свойство
public Int32 SomeProp { // 11
get { return 0; } // 12
set { } //13
}
// Обобщенное экземплярное свойство
public Int32 this[String s] { // 14
get { return 0; } // 15
set { } //16
}
// Экземплярное событие
public event EventHandler SomeEvent; // 17
}
После компиляции типа можно просмотреть метаданные с помощью утилиты ILDasm.exe (рис. 6.1).
Заметьте, что компилятор генерирует метаданные для всех членов типа. На самом деле, для некоторых членов, например для события (17), компилятор создает дополнительные члены (поле и два метода) и метаданные. На данном этапе не требуется точно понимать, что изображено на рисунке, но по мере чтения следующих глав я рекомендую возвращаться к этому примеру и смотреть, как определяется тот или иной член и как это влияет на метаданные, генерируемые компилятором.
Рис. 6.1. Метаданные, полученные с помощью утилиты ILDasm.exe для приведенного примера |
Видимость типа
I Ipn («пределен ни типа с видимостью в рамках файла, а не другого типа его можно сделать открытым (public) или внутренним (internal). Открытый тип доступен любому коду любой сборки. Внутренний тип доступен только в той сборке, где он определен. 11о умолчанию компилятор C# делает тип внутренним (с более ограниченной видимостью). Вот несколько примеров.
using System;
// Открытый тип доступен из любой сборки public class ThisIsAPublicType { ... }
// Внутренний тип доступен только из собственной сборки internal class ThisIsAnlnternalType { ... }
// Это внутренний тип, так как модификатор доступа не указан явно class ThisIsAlsoAnlnternalType { ... }
Дружественные сборки
11редставьте себе следующую ситуацию: в компании есть группа/!, определяющая набор полезных типов в одной сборке, и группа Б, использующая эти типы. По разным причинам, таким как индивидуальные графики работы, географическая разобщенность, различные источники финансирования или структуры подотчетности, эти группы не могут разместить все свои типы в единой сборке; вместо этого в каждой группе создается собственный файл сборки.
Чтобы сборка группы Б могла использовать типы группы А, группа А должна определить все нужные второй группе типы как открытые. Однако это означает, что эти типы будут доступны абсолютно всем сборкам. В результате разработчики другой компании смогут написать код, использующий общедоступные типы, а это нежелательно. Вполне возможно, в полезных типах действуют определенные условия, которым должна соблюдать группа Б при написании кода, использующего типы группы А. То есть нам необходим способ, который бы позволил группе А определить свои типы как внутренние, но в то же время предоставить группе Б доступ к этим типам. Для таких ситуаций в CLR и C# предусмотрен механизм дружественных сборок (friend assemblies). Кстати говоря, он пригодится еще и в ситуации со сборкой, содержащей код, который выполняет модульные тесты с внутренними типами другой сборки.
В процессе создания сборки можно указать другие сборки, которые она будет считать «друзьями», — для этого служит атрибут InternalsVisibleTo, определенный в пространстве имен System. Runtime. CompilerServices. У атрибута есть строковый параметр, определяющий имя дружественной сборки и ее открытый ключ (передаваемая атрибуту строка не должна содержать информацию о версии, региональных стандартах или архитектуре процессора). Заметьте, что дружественные сборки получают доступ ко всем внутренним типам сборки, а также к внутренним членам этих типов. Приведем пример сборки, которая объявляет дружественными две другие сборки со строгими именами Wintellect и Microsoft:
using System;
using System.Runtime.CompilerServices; // Для атрибута InternalsVisibleTo
// Внутренние типы этой сборки доступны из кода двух следующих сборок // (независимо от версии или региональных стандартов)
[assembly:InternalsVisibleTo("Wintellect, PublicKey=12345678...90abcdef”)]
[assembly:InternalsVisibleTo("Microsoft, PublicKey=b77a5c56...1934e089")]
internal sealed class SomelnternalType { ... } internal sealed class AnotherlnternalType { ... }
Обратиться из дружественной сборки к внутренним типам представленной здесь сборки очень просто. Например, дружественная сборка Wintellect с открытым ключом 12345678...90abcdef может обратиться к внутреннему типу SomelnternalType
представленной сборки следующим образом:
using System;
internal sealed class Foo {
private static Object SomeMethodQ {
// Эта сборка Wintellect получает доступ к внутреннему типу // другой сборки, как если бы он был открытым SomelnternalType sit = new SomelnternalTypeQ; return sit;
}
}
Поскольку внутренние члены принадлежащих сборке типов становятся доступными для дружественных сборок, следует очень осторожно подходить к определению уровня доступа, предоставляемого членам своего типа, и объявлению дружественных сборок. Заметьте, что при компиляции дружественной (то есть не содержащей атрибута InternalsVisibleTo) сборки компилятору C# требуется задавать параметр /out: файл. Он нужен компилятору, чтобы узнать имя компилируемой сборки и определить, должна ли результирующая сборка быть дружественной. Можно подумать, что компилятор C# в состоянии самостоятельно выяснить это имя, так как он обычно самостоятельно определяет имя выходного файла; однако компилятор «узнает» имя выходного файла только после завершения компиляции. Поэтому требование указывать этот параметр позволяет значительно повысить производительность компиляции.
Аналогично, при компиляции модуля (в противоположность сборке) с параметром /t: module, который должен стать частью дружественной сборки, необходимо также использовать параметр /moduleassemblyname:строка компилятора С#. Последний параметр говорит компилятору, к какой сборке будет относиться модуль, чтобы тот разрешил коду этого модуля обращаться к внутренним типам другой сборки.
Доступ к членам типов
При определении члена типа (в том числе вложенного) можно указать модификатор доступа к члену. Модификаторы определяют, на какие члены можно ссылаться из кода. В CLR имеется собственный набор возможных модификаторов доступа, но в каждом языке программирования существуют свои синтаксис и термины. Например, термин Assembly в CLR указывает, что член доступен изнутри сборки, тогда как в C# для этого используется ключевое слово internal.
В табл. 6.1 представлено шесть модификаторов доступа, определяющих уровень ограничения — от максимального (Private) до минимального (Public).
Таблица 6.1. Модификаторы доступа к членам
продолжение &
|
Таблица 6.1 (продолжение)
|
|
Разумеется, для получения доступа к члену типа он должен быть определен в видимом типе. Например, если в сборке А определен внутренний тип, имеющий открытый метод, то код сборки В не сможет вызвать открытый метод, поскольку внутренний тип сборки А недоступен из В.
В процессе компиляции кода компилятор языка проверяет корректность обращения кода к типам и членам. Обнаружив некорректную ссылку на какие-либо типы или члены, компилятор информирует об ошибке. Помимо этого, во время выполнения JIT-компилятор тоже проверяет корректность обращения к полям и методам при компиляции IL-кода в процессорные команды. Например, обнаружив код, неправильно пытающийся обратиться к закрытому полю или методу, JIT-компилятор генерирует исключение FieldAccessException HHHMethodAccessException соответственно.
Верификация IL-кода гарантирует правильность обработки модификаторов доступа к членам в период выполнения, даже если компилятор языка проигнорировал проверку доступа. Другая, более вероятная возможность заключается в компиляции кода, обращающегося к открытому члену другого типа (другой сборки); если в период выполнения загрузится другая версия сборки, где модификатор доступа открытого члена заменен защищенным (protected) или закрытым (private), верификация обеспечит корректное управление доступом.
Если модификатор доступа явно не указан, компилятор C# обычно (но не всегда) выберет по умолчанию закрытый — наиболее строгий из всех. CLR требует, чтобы все члены интерфейсного типа были открытыми. Компилятор C# знает об этом и запрещает программисту явно указывать модификаторы доступа к членам интерфейса, просто делая все члены открытыми.
ПРИМЕЧАНИЕ
Подробнее о правилах применения в C# модификаторов доступа к типам и членам, а также о том, какие модификаторы C# выбирает по умолчанию в зависимости от контекста объявления, рассказывается в разделе «Declared Accessibility» спецификации языка С#.
Более того, как видно из табл. 6.1, в CLR есть модификатор доступа родовой и сборочный. Но разработчики C# сочли этот атрибут лишним и не включили в язык С#.
Если в производном типе переопределяется член базового типа, компилятор C# требует, чтобы у членов базового и производного типов были одинаковые модификаторы доступа. То есть если член базового класса является защищенным, то и член производного класса должен быть защищенным. Однако это ограничение языка С#, а не CLR. При наследовании от базового класса CLR позволяет снижать, но не повышать ограничения доступа к члену. Например, защищенный метод базового класса можно переопределить в производном классе в открытый, но не в закрытый. Дело в том, что пользователь производного класса всегда может получить доступ к методу базового класса путем приведения к базовому типу. Если бы среда CLR разрешала устанавливать более жесткие ограничения на доступ к методу в производном типе, то эти ограничения бы элементарно обходились.
Статические классы
Существуют классы, не предназначенные для создания экземпляров, например Console, Math, Environment и ThreadPool. У этих классов есть только статические методы. В сущности, такие классы существуют лишь для группировки логически связанных членов. Например, класс Math объединяет методы, выполняющие математические операции. В C# такие классы определяются с ключевым словом static. Его разрешается применять только к классам, но не к структурам (значимым типам), поскольку CLR всегда разрешает создавать экземпляры значимых типов, и нет способа обойти это ограничение.
Компилятор налагает на статический класс ряд ограничений.
□ Класс должен быть прямым потомком System. Object — наследование любому другому базовому классу лишено смысла, поскольку наследование применимо только к объектам, а создать экземпляр статического класса невозможно.
□ Класс не должен реализовывать никаких интерфейсов, поскольку методы интерфейса можно вызывать только через экземпляры класса.
□ В классе можно определять только статические члены (поля, методы, свойства и события). Любые экземплярные члены вызовут ошибку компиляции.
□ Класс нельзя использовать в качестве поля, параметра метода или локальной переменной, поскольку это подразумевает существование переменной, ссылающейся на экземпляр, что запрещено. Обнаружив подобное обращение со статическим классом, компилятор вернет сообщение об ошибке.
Приведем пример статического класса, в котором определены статические члены; сам по себе класс не представляет практического интереса.
194 Глава 6. Основные сведения о членах и типах using System;
public static class AStaticClass {
public static void AStaticMethodQ { }
public static String AStaticProperty { get { return sAStaticField; } set { s_AStaticField = value; }
}
private static String s_AStaticField; public static event EventHandler AStaticEvent;
}
Ha puc, 6.2 приведен результат дизассемблирования с помощью утилиты ILDasm.exe библиотечной (DLL) сборки, полученной при компиляции приведенного фрагмента кода. Как видите, определение класса с ключевым словом static заставляет компилятор C# сделать этот класс абстрактным (abstract) и запечатанным (sealed). Более того, компилятор не создает в классе метод конструктора экземпляров (.ctor).
Рис. 6.2. Статический класс в ILDasm.exe |
Частичные классы, структуры и интерфейсы
Ключевое слово partial говорит компилятору С#, что исходный код класса, структуры или интерфейса может располагаться в нескольких файлах. Компилятор
объединяет все частичные файлы класса во время компиляции; CLR всегда работает с полными определениями типов. Есть три основные причины, по которым исходный код разбивается на несколько файлов.
□ Управление версиями. Представьте, что определение типа содержит большой объем исходного кода. Если этот тип будут одновременно редактировать два программиста, по завершении работы им придется каким-то образом объединять свои результаты, что весьма неудобно. Ключевое слово partial позволяет разбить исходный код типа на несколько файлов, чтобы один и тот же тип могли одновременно редактировать несколько программистов.
□ Разделение файла или структуры на логические модули внутри файла.
Иногда требуется создать один тип для решения разных задач. Для упрощения реализации я иногда объявляю одинаковые типы повторно внутри одного файла. Затем в каждой части такого типа я реализую по одному функциональному аспекту типа со всеми его полями, методами, свойствами, событиями и т. д. Это позволяет мне упростить наблюдение за членами, обеспечивающими единую функциональность и объединенными в группу. Я также могу легко закомментировать частичный тип с целью удаления всей функциональности из класса или замены ее другой (путем использования новой части частичного типа).
□ Разделители кода. При создании в Microsoft Visual Studio нового проекта Windows Forms или Web Forms некоторые файлы с исходным кодом создаются автоматически. Они называются шаблонными. При использовании конструкторов форм Visual Studio в процессе создания и редактирования элементов управления формы автоматически генерирует весь необходимый код и помещает его в отдельные файлы. Это значительно повышает продуктивность работы. Раньше автоматически генерируемый код попадал в тот же файл, где программист писал свой исходный код. Однако при случайном изменении сгенерированного кода конструктор форм переставал корректно работать. Начиная с Visual Studio 2005, при создании нового проекта Visual Studio создает два исходных файла: один предназначен для программиста, а в другой помещается код, создаваемый редактором форм. В результате вероятность случайного изменения генерируемого кода существенно снижается.
Ключевое слово partial применяется к типам во всех файлах с определением типа. При компиляции компилятор объединяет эти файлы, и готовый тип помещается в результирующий файл сборки с расширением ехе или dll (или в файл модуля с расширением netmodule). Как уже отмечалось, частичные типы реализуются только компилятором С#; поэтому все файлы с исходным кодом таких типов необходимо писать на одном языке и компилировать их вместе как единый блок компиляции.
Компоненты, полиморфизм и версии
Объектно-ориентированное программирование (ООП) существует уже много лет. В поздние 70-е и ранние 80-е годы объектно-ориентированные приложения были намного меньше по размеру, и весь код приложения разрабатывался в одной компании. Разумеется, в то время уже были операционные системы, позволяющие приложениям по максимуму использовать их возможности, но современные ОС предлагают намного больше функций.
Сложность программного обеспечения существенно возросла, к тому же пользователи требуют от приложений богатых функциональных возможностей — графического интерфейса, меню, различных устройств ввода-вывода (мышь, принтер, планшет), сетевых функций и т. п. Все это привело к существенному расширению функциональности операционных систем и платформ разработки в последние годы. Более того, сейчас уже не представляется возможным или эффективным писать приложение «с нуля» и разрабатывать все необходимые компоненты самостоятельно. Современные приложения состоят из компонентов, разработанных многими компаниями. Эти компоненты объединяются в единое приложение в рамках парадигмы ООП.
При компонентной разработке приложений (Component Software Programming, CSP) идеи ООП используются на уровне компонентов. Ниже перечислены некоторые свойства компонента.