Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
CheckBootSpeed - это диагностический пакет на основе скриптов PowerShell, создающий отчет о скорости загрузки Windows 7 ...
Вы когда-нибудь хотели создать установочный диск Windows, который бы автоматически установил систему, не задавая вопросо...
Если после установки Windows XP у вас перестала загружаться Windows Vista или Windows 7, вам необходимо восстановить заг...
Программа подготовки документов и ведения учетных и отчетных данных по командировкам. Используются формы, утвержденные п...
Red Button – это мощная утилита для оптимизации и очистки всех актуальных клиентских версий операционной системы Windows...

Новые средства C# в .NET Framework 4

Текущий рейтинг: 3.38 (проголосовало 8)
 Посетителей: 4154 | Просмотров: 7889 (сегодня 0)  Шрифт: - +

С момента своего появления в 2002 году язык программирования C# был значительно усовершенствован, чтобы программисты могли писать более четкий и простой в сопровождении код. Улучшения включали такие языковые средства, как обобщенные типы, значимые типы, допускающие null-значения, лямбда-выражения, методы итератора, частичные классы и длинный список других полезных языковых конструкций. И зачастую эти изменения сопровождались введением соответствующей поддержки в библиотеки Microsoft .NET Framework.

Эта тенденция в сторону повышения удобства в использовании сохраняется и в C# 4.0. Внесенные в него изменения намного упрощают выполнение распространенных задач, в том числе работу с обобщенными типами, взаимодействие с устаревшими инфраструктурами и операции с моделями динамических объектов. Цель этой статьи — дать вам высокоуровневый обзор всех этих новых средств. Я начну с обобщенной вариантности (generic variance), а затем рассмотрю средства взаимодействия с устаревшими инфраструктурами и динамическими объектами.

Ковариантность и контравариантность

Суть ковариантности (covariance) и контравариантности (contravariance) лучше всего показать на примере, а лучший пример уже присутствует в самой инфраструктуре. IEnumerable<T> и IEnumerator <T> в System.Collections.Generic представляют соответственно объект, который является последовательностью T, и перечислитель (или итератор), выполняющий перебор последовательности. Эти интерфейсы уже давно и интенсивно используются, так как они поддерживают реализацию конструкции цикла foreach. В C# 3.0 они стали еще важнее ввиду того, что они играют центральную роль в LINQ и LINQ to Objects, — это .NET-интерфейсы для представления последовательностей.

Если у вас есть иерархия классов, скажем, с типом Employee и типом Manager, производным от Employee (менеджеры — ведь тоже наемные работники), то, как по-вашему, что делает следующий код?

IEnumerable<Manager> ms = GetManagers();
IEnumerable<Employee> es = ms;

Выглядит так, что последовательность Manager следует интерпретировать как последовательность Employee. Но в C# 3.0 это присваивание завершится неудачей; компилятор сообщит, что преобразование типов отсутствует. В конце концов, у него нет ни малейшего представления о должной в данном случае семантике IEnumerable<T>. Это может быть любой интерфейс, поэтому, если взять произвольный интерфейс IFoo<T>, с какой стати IFoo<Manager> должен быть правильнее, чем IFoo<Employee>?

Однако в C# 4.0 это присваивание сработает, так как IEnumerable<T> (наряду с несколькими другими интерфейсами) изменился из-за новой в C# поддержки ковариантности параметров-типов.

IEnumerable<T> наделен более специфическими правами, чем произвольный IFoo<T>, поскольку — хоть это и не очевидно с первого взгляда — члены, использующие параметр-тип T (GetEnumerator в IEnumerable<T> и свойство Current в IEnumerator<T>), на самом деле применяют T только в позиции возвращаемого значения. А значит, вы лишь получаете Manager из последовательности, но никогда не помещаете его туда.

А теперь для контраста вспомните List<T>. Сделав List<Manager> заменителем List<Employee>, вы устроили бы катастрофу ввиду следующего:

List<Manager> ms = GetManagers();
List<Employee> es = ms; // Suppose this were possible
es.Add(new EmployeeWhoIsNotAManager()); // Uh oh

Пример показывает: как только вы начинаете думать, будто смотрите на List<Employee>, вы можете вставить любого сотрудника. Но данный список на самом деле является List<Manager>, поэтому вставка сотрудника, отличного от менеджера, должна завершиться ошибкой. Иначе вы потеряли бы безопасность системы типов. List<T> не может быть ковариантным в T.

В таком случае это новое языковое средство в C# 4.0 заключается в возможности определять типы вроде нового IEnumerable<T>, который допускает преобразования между собой при условии, что его параметры-типы имеют некое отношение друг к другу. Вот что использовали разработчики .NET Framework, которые писали IEnumerable<T>, и вот как выглядел их код (в упрощенном виде, конечно):

public interface IEnumerable<out T> { /* ... */ }

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

Почему же это назвали ковариантностью? Ну, легче всего это понять, рисуя связи стрелками. Конкретнее, возьмем типы Manager и Employee. Поскольку между этими классами существует связь наследования, допускается неявное ссылочное преобразование (implicit reference conversion) из Manager в Employee:

Manager → Employee

И теперь из-за аннотации T в IEnumerable<out T> существует еще и неявное ссылочное преобразование из IEnumerable<Manager> в IEnumerable<Employee>. Вот для чего предоставляется аннотация:

IEnumerable<Manager> → IEnumerable<Employee>

Это называют ковариантностью потому, что стрелки в каждом из двух примеров указывают в одном направлении. Мы начали с двух типов: Manager и Employee. Мы сделали из них новые типы: IEnumerable<Manager> и IEnumerable<Employee>. Новые типы преобразуются так же, как и исходные.

Под контравариантностью подразумевают обратное. Вероятно, вы уже догадались, что это возможно, когда параметр-тип T используется только как входной, и вы совершенно правы. Например, в пространстве имен System содержится интерфейс IComparable<T> с единственным методом CompareTo:

public interface IComparable<in T> {
  bool CompareTo(T other);
}

Если у вас есть IComparable<Employee>, вы должны иметь возможность обрабатывать его так, будто это IComparable<Manager>, поскольку единственное, что вы можете сделать, — передать Employee в интерфейс. А раз менеджер является и сотрудником, такая передача должна работать, и она работает. В этом случае параметр-тип T модифицируется ключевым словом in, и в следующем примере функционирует корректно:

IComparable<Employee> ec = GetEmployeeComparer();
IComparable<Manager> mc = ec;

Это называют контравариантностью потому, что на этот раз стрелка указывает в обратном направлении:

Manager → Employee
IComparable<Manager> ← IComparable<Employee>

Итак, добавляя ключевое слово in или out при определении параметра-типа, вы вольны выполнять дополнительные преобразования. Но некоторые ограничения все же есть.

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

Во-вторых, при наличии интерфейса или делегата с ковариантным или контравариантным параметром-типом вы получаете право на новые преобразования этого типа лишь при условии, что аргументы типа в случае использования интерфейса (а не его определения) являются ссылочными типами. Например, так как int — значимый тип, IEnumerator<int> нельзя преобразовать в IEnumerator <object>, хотя кажется, что это следовало бы разрешить:

IEnumerator <int> ->  IEnumerator <object>

Причина такого поведения в том, что представление типа должно сохраниться после преобразования. Если бы было разрешено преобразование int в object, вызов свойства Current стал бы в итоге невозможен, так как значимый тип int имеет другое представление в стеке по сравнению с объектной ссылкой. Однако все ссылочные типы представляются в стеке одинаково, поэтому лишь над аргументами ссылочных типов допускаются эти дополнительные преобразования.

Весьма вероятно, большинство разработчиков на C# будет с удовольствием пользоваться этим новым языковым средством — это позволит им применять больше преобразований над типами, встроенными в инфраструктуру, и реже получать ошибки компиляции при использовании некоторых типов из .NET Framework (в том числе IEnumerable<T>, IComparable<T>, Func<T>, Action<T>). И фактически любой, кто проектирует библиотеку с обобщенными интерфейсами и делегатами, волен применять новые параметры-типы с in и out, облегчая жизнь своим пользователям.

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

Динамическая диспетчеризация

Теперь перейдем к средствам взаимодействия в C# 4.0 и начнем, пожалуй, с самого главного изменения.

В C# введена поддержка динамического позднего связывания (dynamic late-binding). Этот язык всегда был строго типизированным и остается таковым в версии 4.0. В Microsoft уверены, что это делает C# простым в использовании, быстрым и подходящим для любой работы, которой занимаются .NET-программисты. Но иногда возникает необходимость во взаимодействии с системами, не основанными на .NET.

К решению этой проблемы традиционно подходили по крайней мере двумя способами. Первый заключался в простом импорте внешней модели прямо в .NET как прокси. Механизм COM Interop как раз и является одним из примеров этого подхода. В .NET Framework эту стратегию применяли с момента первого выпуска, при этом использовали утилиту TLBIMP, которая создает новые .NET-типы прокси, доступные напрямую из C#.

LINQ-to-SQL, поставляемая с C# 3.0, содержит утилиту SQLMETAL, которая импортирует существующую базу данных в C#-классы прокси для использования с запросами. Кроме того, есть утилита, импортирующая WMI-классы (Windows Management Instrumentation) в C#. Многие технологии позволяют писать код на C# (часто с атрибутами), а затем осуществлять взаимодействие с помощью этого кода для выполнения внешних операций, применяя, например, LINQ-to-SQL, Windows Communication Foundation (WCF) и сериализацию.

Второй подход полностью отвергает систему типов C#: вы встраиваете в свой код строки и данные. Именно так вы поступаете всякий раз, когда пишете код, который, скажем, вызывает какой-либо метод JScript-объекта, или когда вы встраиваете SQL-запрос в свое приложение на основе ADO.NET. Вы делаете это, даже когда откладываете связывание до периода выполнения, используя механизм отражения, хотя в данном случае взаимодействие осуществляется с самой .NET.

Ключевое слово dynamic в C# устраняет все сложности, связанные с упомянутыми подходами. Начнем с простого примера — отражения. Обычно его применение требует написания уймы стереотипного инфраструктурного кода вроде показанного ниже:

object o = GetObject();
Type t = o.GetType();
object result = t.InvokeMember("MyMethod",
  BindingFlags.InvokeMethod, null,
  o, new object[] { });
int i = Convert.ToInt32(result);

Теперь вместо вызова метода MyMethod некоего объекта с использованием отражения вы можете с помощью ключевого слова dynamic сообщить компилятору обрабатывать o как динамический и отложить весь анализ до периода выполнения. Код, который делает это, выглядит так:

dynamic o = GetObject();
int i = o.MyMethod();

Этот код работает и выполняет ровным счетом то же самое, но обратите внимание, насколько он проще и четче.

Ценность этого более лаконичного и простого синтаксиса, вероятно, проявится гораздо нагляднее, если вы посмотрите на класс ScriptObject, который поддерживает операции с JScript-объектом. В этом классе есть метод InvokeMember с большим количеством параметров; исключение составляет Silverlight, где аналогичный метод называется Invoke (обратите внимание на различие в именах) с меньшим числом параметров. Ни один из них не годится для вызова метода IronPython- или IronRuby-объекта, а также любого объекта, не относящегося к C#, с которым вам может понадобиться взаимодействие.

В дополнение к объектам из динамических языков вы найдете самые разнообразные модели данных, которые по своей природе являются динамическими и имеют разные API, поддерживающие их, например HTML DOM, System.Xml DOM и модель XLinq для XML. COM-объекты тоже зачастую являются динамическими.

Фактически C# 4.0 предлагает упрощенное согласованное представление динамических операций. Чтобы воспользоваться этим преимуществом, вам надо лишь указать, что данное значение динамическое, и анализ всех операций над этим значением будет отложен до периода выполнения.

В C# 4.0 тип dynamic является встроенным и обозначается специальным псевдо ключевым словом. Однако dynamic отличается от var. Переменные, объявленные с var, на самом деле подчиняются строгой типизации — просто тем самым программист перекладывает бремя определения их типов на компилятор. Когда программист использует dynamic, компилятору не известен тип, который будет присвоен данной переменной, — эта задача откладывается до периода выполнения.

Dynamic и DLR

Инфраструктура, поддерживающая эти динамические операции в период выполнения, называется Dynamic Language Runtime (DLR). Это новая библиотека .NET Framework 4, выполняемая CLR, как и любые другие управляемые библиотеки. DLR служит посредником при выполнении каждой динамической операции между языком, из которого инициирована эта операция, и объектом, к которому она применяется. Если динамическая операция не обработана объектом, в котором она протекает, один из компонентов компилятора C# обрабатывает связывание. Упрощенная и неполная схема архитектуры показана на рис. 1.

*

Рис. 1. DLR выполняется поверх CLR

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

dynamic d = new MyDynamicObject();
d.Bar("Baz", 3, d);

Если MyDynamicObject был определен, как показано здесь, то вы можете представить, что произойдет:

class MyDynamicObject : DynamicObject {
  public override bool TryInvokeMember(
    InvokeMemberBinder binder,
    object[] args, out object result) {

    Console.WriteLine("Method: {0}", binder.Name);
    foreach (var arg in args) {
      Console.WriteLine("Argument: {0}", arg);
    }

    result = args[0];
    return true;
  }
}

Фактически этот код выведет:

Method: Bar
Argument: Baz
Argument: 3
Argument: MyDynamicObject

Так как переменная d объявлена с динамическим типом, код, использующий мой экземпляр MyDynamicObject, в конечном счете не принимает участия на этапе компиляции в проверке операций, в которых участвует d. Применение ключевого слова dynamic означает: «Я не знаю, какой тип будет у этой переменной, а потому мне заранее не известно, какие методы или свойства у нее есть. Компилятор, пожалуйста, пропусти их всех и разберись с ними потом, когда у тебя появится реальный объект в период выполнения». Поэтому вызов Bar компилируется даже несмотря на то, что компилятор не знает, что это такое. Впоследствии при выполнении у самого объекта будет запрошено, что делать с этим вызовом Bar. И именно TryInvokeMember знает, как это обрабатывать.

Теперь, допустим, что вместо MyDynamicObject вы воспользовались объектом из Python:

dynamic d = GetPythonObject();
d.bar("Baz", 3, d);

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

def bar(*args):
  print "Method:", bar.__name__
  for x in args:
    print "Argument:", x

За кулисами при каждом использовании динамического значения компилятор генерирует блок кода, который инициализирует и использует DLR CallSite. Этот CallSite содержит всю информацию, необходимую для связывания в период выполнения, в том числе имя метода, дополнительные данные, например происходит ли данная операция в проверяемом контексте, и сведения об аргументах и их типах.

Этот код, если бы вам пришлось самостоятельно поддерживать его, был бы таким же громоздким и уродливым, как и код с отражением, показанный ранее, код ScriptObject или строки, содержащие XML-запросы. В этом вся суть dynamic в C#:

вам больше не нужно писать подобный код! При использовании ключевого слова dynamic ваш код приобретает практически нормальный вид, аналогичный простому вызову метода, вызову индексатора, оператору вроде +, приведению или даже составным операторам наподобие += или ++. Динамические значения можно указывать и в выражениях, например if(d) и foreach(var x in d). Также поддерживается короткое замыкание (short-circuiting), и тогда код выглядит как d && ShortCircuited или d ?? ShortCircuited.

Ценность DLR, предоставляющей общую инфраструктуру для операций такого рода, заключается в том, что вам больше не требуется иметь дело с разными API для каждой динамической модели, которую вы хотели бы использовать в своем коде, — все сведено в единый API. И вам даже не нужно использовать его. Компилятор C# может использовать его за вас, и это позволит вам больше времени уделять своим реальным задачам: чем меньше инфраструктурного кода пишет программист, тем выше производительность его труда.

В языке C# нет сокращений для определения динамических объектов. Ключевое слово dynamic в C# — это все, что необходимо для использования динамических объектов. Рассмотрим следующий код:

dynamic list = GetDynamicList();
dynamic index1 = GetIndex1();
dynamic index2 = GetIndex2();
string s = list[++index1, index2 + 10].Foo();

Этот код компилируется, и он содержит массу динамических операций. Сначала осуществляется динамическое присваивание начального значения index1, а затем динамическое сложение с index2. Далее вызывается динамический индексатор для списка. Результат этих операций вызывает член Foo. Наконец, финальный результат выражения преобразуется в строку и хранится в s. В одной только строке целых пять динамических операций, каждая из которых диспетчеризуется в период выполнения.

Тип периода компиляции каждой динамической операции сам является динамическим, и поэтому «динамичность» переходит от одного вычисления к другому. Даже если бы вы не включали динамические выражения несколько раз, все равно вы получили бы целый ряд динамических операций. В этой (единственной) строке по-прежнему пять динамических операций:

string s = nonDynamicList[++index1, index2 + 10].Foo();

Поскольку результаты двух выражений с индексами динамические, сам index является таковым. И поскольку получаемый в результате индекс динамический, таковым оказывается и вызов Foo. Затем вы должны преобразовать динамическое значение в строку. Разумеется, это тоже выполняется динамически, так как объект может быть динамическим, которому нужно выполнить некие специфические вычисления перед запросом на преобразование.

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

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

Вы можете наблюдать это, попытавшись выполнить преобразование между обобщенными типами, которые различаются только в dynamic и object; такие преобразования всегда срабатывают, так как в период выполнения экземпляр List<dynamic> на самом деле является экземпляром List<object>:

List<dynamic> ld = new List<object>();

Кроме того, вы можете заметить сходство между dynamic и object, если попробуете переопределить какой-нибудь метод, объявленный с объектным параметром:

class C {
  public override bool Equals(dynamic obj) {
    /* ... */
  }
}

Хотя в сборке это приводит к разрешению в дополненный объект (decorated object), я предпочитаю рассматривать dynamic как реальный тип, поскольку он служит своего рода памяткой насчет того, что вы можете делать с ним почти все то же, что и с любым другим типом. Вы можете использовать его как аргумент типа или, скажем, как возвращаемое значение. Например, следующее определение функции позволит вам использовать результат вызова этой функции динамически без предварительной записи возвращаемого значения в динамическую переменную:

public dynamic GetDynamicThing() {
  /* ... */ }

В интерпретации и диспетчеризации dynamic куда больше деталей, чем я успел упомянуть здесь, но вам незачем знать о них, чтобы просто использовать эту функциональность. Самое главное в том, что вы можете писать код, похожий на C#, а если какая-то из его частей является динамической, компилятор оставит ее в покое до периода выполнения.

И последний вопрос, который относится к dynamic и который я хотел бы рассмотреть в этой статье, — обработка ошибок. Так как компилятор не проверяет в динамической сущности, действительно ли у нее есть вызываемый вами метод Foo, он не в состоянии сообщить вам о какой-либо ошибке. Конечно, это не означает, что ваш вызов Foo не сработает в период выполнения. Он может сработать, но существует масса объектов, у которых нет никакого метода Foo. Когда ваше выражение не удается связать в период выполнения, механизм связывания (binder) в таком случае предпринимает максимум усилий сгенерировать для вас исключение, которое более-менее точно отражает то, что компилятор сказал бы вам в отсутствие ключевого слова dynamic.

Возьмем следующий код:

try
{
  dynamic d = "this is a string";
  d.Foo();
}
catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e)
{
  Console.WriteLine(e.Message);
}

Здесь у меня есть строка, а у строк точно нет метода Foo. При выполнении кода, вызывающего Foo, связывание завершится неудачей, и вы получите исключение RuntimeBinderException. Это и выводит предыдущая программа:

'string' does not contain a definition for 'Foo'

То есть вы получаете такое сообщение об ошибке, которое и следовало бы ожидать программисту на C#.

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

Благодаря еще одному дополнению для C# методы теперь поддерживают необязательные параметры со значениями по умолчанию, поэтому при вызове такого метода вы можете опускать подобные параметры. Посмотреть на это в действии можно в следующем классе Car:

class Car {
  public void Accelerate(
    double speed, int? gear = null,
    bool inReverse = false) {

    /* ... */
  }
}

Вы можете вызвать метод так:

Car myCar = new Car();
myCar.Accelerate(55);

Это даст точно такой же эффект, что и следующее:

myCar.Accelerate(55, null, false);

Эффект одинаков, потому что компилятор вставит все пропущенные вами значения по умолчанию.

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

Допустим, вы хотите вызвать Accelerate для переключения на обратную передачу (in reverse), но не желаете указывать параметр gear. Что ж, можно сделать и так:

myCar.Accelerate(55, inReverse: true);

Это новый синтаксис в C# 4.0, и он идентичен следующему:

myCar.Accelerate(55, null, true);

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

Console.WriteLine(format: "{0:f}", arg0: 6.02214179e23);
Console.WriteLine(arg0: 6.02214179e23, format: "{0:f}");

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

С первого взгляда именованные аргументы и необязательные параметры не имеют отношения к средствам взаимодействия. Вы можете пользоваться ими, даже никогда не задумываясь о каком-либо механизме взаимодействия. Однако мотивация разработки такой функциональности кроется в Office API. Возьмем, к примеру, программирование Word и что-нибудь простенькое вроде метода SaveAs из интерфейса Document. У этого метода 16 параметров, и все они необязательные. В предыдущих версиях C#, если бы вы захотели вызвать этот метод, вам пришлось бы писать примерно такой код:

Document d = new Document();
object filename = "Foo.docx";
object missing = Type.Missing;
d.SaveAs(ref filename, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing);

А теперь достаточно:

Document d = new Document();
d.SaveAs(FileName: "Foo.docx");

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

Теперь, когда вы пишете .NET-библиотеку и подумываете о добавлении методов с необязательными параметрами, перед вами встает выбор. Либо вы добавляете необязательные параметры, либо поступаете так, как годами делали программисты на C#: вводите перегруженные версии. В примере с Car.Accelerate последнее решение может привести вас к созданию типа, который выглядит примерно так:

class Car {
  public void Accelerate(uint speed) {
    Accelerate(speed, null, false);
  }
  public void Accelerate(uint speed, int? gear) {
    Accelerate(speed, gear, false);
  }
  public void Accelerate(uint speed, int? gear,
    bool inReverse) {
    /* ... */
  }
}

Выбор модели, подходящей для создаваемой вами библиотеки, возлагается исключительно на вас. Поскольку в C# до сих пор не было необязательных параметров, в .NET Framework (в том числе в .NET Framework 4) наблюдается тенденция к применению перегруженных версий. Если вы решите смешать эти подходы и сопоставить перегруженные версии с необязательными параметрами, механизм разрешения перегруженных версий в C# подчиняется четким правилам, определяющим, какая перегруженная версия будет вызываться в конкретных обстоятельствах.

Индексируемые свойства

Некоторые новые менее значимые языковые средства в C# 4.0 поддерживаются, только когда код пишется в расчете на COM Interop API. Один из примеров — взаимодействие с Word в предыдущих фрагментах кода.

В коде на C# всегда использовалась нотация индексатора, который вы можете добавить к какому-нибудь классу для эффективной перегрузки оператора [] в экземплярах этого класса. Эта форма индексатора также называется индексатором по умолчанию, поскольку ему не присваивается имя и его можно вызывать, не указывая какое-либо имя. Однако в некоторых COM API есть индексаторы, отличные от таковых по умолчанию, которые нельзя вызывать простым использованием [] — вы должны указывать конкретное имя. В качестве альтернативы вы можете считать, что индексируемое свойство — это такое свойство, которое принимает некоторые дополнительные аргументы.

C# 4.0 поддерживает индексируемые свойства в типах COM Interop. Определять типы с индексируемыми свойствами в C# нельзя, но можно использовать их применительно к любому COM-типу. Как выглядит код на C#, способный делать это, мы рассмотрим на примере свойства Range рабочего листа Excel:

using Microsoft.Office.Interop.Excel;

class Program {
  static void Main(string[] args) {
    Application excel = new Application();
    excel.Visible = true;

    Worksheet ws =
      excel.Workbooks.Add().Worksheets["Sheet1"];
    // Range is an indexed property
    ws.Range["A1", "C3"].Value = 123;
    System.Console.ReadLine();
    excel.Quit();
  }
}

В этом примере Range["A1", "C3"] является свойством Range, которое возвращает нечто, что можно индексировать. Это один вызов аксессора Range, в котором передаются A1 и C3. И хотя Value не выглядит индексируемым свойством, на самом деле оно тоже является таковым! Все его аргументы необязательны, а поскольку это индексируемое свойство, вы можете опускать их. До введения в язык поддержки индексируемых свойств вам пришлось бы писать примерно такой вызов:

ws.get_Range("A1", "C3").Value2 = 123;

Здесь Value2 — свойство, добавленное просто потому, что индексируемое свойство Value не сработало бы в C# до версии 4.0.

Пропуск ключевого слова Ref в COM CallSite

Некоторые COM API принимают множество параметров, передаваемых по ссылке, даже когда конкретная реализация ничего не записывает в них. В пакете Office наиболее ярким примером может служить Word — все его COM API делают это.

Когда вы сталкиваетесь с такой библиотекой и вам нужно передавать аргументы по ссылке, вы теряете возможность передавать любое выражение, которое не является локальной переменной или полем, а это серьезная головная боль. В примере с Word SaveAs вы можете увидеть это на практике: вам пришлось объявлять локальные переменные filename и missing только для того, чтобы вызвать метод SaveAs, так как эти параметры нужно передавать по ссылке:

Document d = new Document();
object filename = "Foo.docx";
object missing = Type.Missing;
d.SaveAs(ref filename, ref missing, // ...

Вероятно, вы заметили в новом коде на C#, который был расположен ниже, что я больше не объявлял локальную переменную для имени файла:

d.SaveAs(FileName: "Foo.docx");

Это стало возможным благодаря новому механизму, который позволяет опускать ref при работе с COM Interop. Теперь при вызове метода через COM Interop вы можете передавать любой аргумент по значению, а не по ссылке. В этом случае компилятор создает временную локальную переменную за вас и передает ее по ссылке, если это требуется. Конечно, вы не сможете увидеть результат такого вызова метода, если этот метод изменяет аргумент; тогда вам придется передавать аргумент по ссылке.

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

Встраивание типов COM Interop

Это в большей мере механизм компилятора C#, нежели языковое средство C#, но теперь вы можете использовать какую-либо сборку COM Interop без ее присутствия в период выполнения. Цель — уменьшить бремя развертывания сборок COM Interop вместе с вашим приложением.

Вводя COM Interop в исходную версию .NET Framework, разработали концепцию Primary Interop Assembly (PIA). Это было попыткой решить проблему совместного использования COM-объектов разными компонентами. Если у вас были разные interop-сборки, которые определяли Excel Worksheet, вы не могли разделять эти Worksheet между компонентами, так как все они были разных .NET-типов. PIA устраняла эту проблему — ее использовали все клиенты, и .NET-типы всегда совпадали.

Хотя идея была отличной на бумаге, на практике развертывание PIA превратилось в большую проблему, так как множество приложений пыталось установить или удалить эту сборку. Дело осложнялось тем, что PIA зачастую были весьма большими, Office не устанавливал их по умолчанию, а пользователи могли легко обойти эту систему единой сборки, просто задействовав утилиту TLBIMP и создав собственную interop-сборку.

Итак, теперь для исправления этой ситуации делается следующее:

  • • исполняющей среде передаются так называемые смарты (smarts) для обработки двух структурно идентичных типов COM Interop, совместно использующих одинаковые идентификационные характеристики (имя, GUID и др.), — как будто это один и тот же .NET-тип;
  • • компилятор C# использует преимущества этого, просто воспроизводя interop-типы в вашей сборке при компиляции, исключая необходимость в наличии interop-сборки в период выполнения.

Я вынужден опустить ряд деталей из-за нехватки места, но, даже не зная всех деталей, вы сможете использовать этот механизм без всяких проблем — полная аналогия с dynamic. Вы сообщаете компилятору встраивать interop-типы за вас и для этого в Visual Studio устанавливаете свойство Embed Interop Types для своей ссылки в True.

Поскольку группа C# считает это предпочтительным способом добавления ссылок на COM-сборки, Visual Studio установит это свойство в True по умолчанию для любой новой interop-ссылки, добавляемой в проект на C#. Если вы используете компилятор командной строки (csc.exe) для сборки своего кода, то для встраивания interop-типов вы должны ссылаться на нужную interop-сборку, используя ключ /L вместо /R.

Каждое из средств, рассмотренных мной в этой статье, могло бы повлечь за собой куда более глубокое обсуждение, и все они заслуживают отдельных статей. Я опустил или мельком упомянул очень многие детали, но, надеюсь, моя статья послужит вам неплохой отправной точкой в изучении C# 4.0. И если вы прочитаете ее, надеюсь, вам понравятся улучшения производительности и читаемости программы, спроектированные специально для вас.

Автор: Крис Берроуз  •  Иcточник: Журнал MSDN  •  Опубликована: 24.01.2011
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER
Теги:   .NET Framework 4.


Оценить статью:
Вверх
Комментарии посетителей
Комментарии отключены. С вопросами по статьям обращайтесь в форум.