Продолжается подписка на наши издания! Вы не забыли подписаться?

Новое в C# 4.0

Автор: Герберт Шилдт
Опубликовано: 30.12.2010
Версия текста: 1.1
Ковариантность и контравариантность в параметрах обобщенного типа
Применение ковариантности в обобщенном интерфейсе
Применение контравариантности в обобщенном интерфейсе
Вариантные делегаты
Создание экземпляров объектов обобщенных типов
Создание объектов динамического типа
Возможность взаимодействия с моделью COM
Дружественные сборки
Исследование возможностей PLINQ
Класс ParallelEnumerable
Распараллеливание запроса методом AsParallel()
Применение метода AsOrdered()
Отмена параллельного запроса
Другие средства PLINQ
Вопросы эффективности PLINQ

ПРИМЕЧАНИЕ

В новом исчерпывающем справочнике "C# 4.0. Полное руководство" детально рассмотрены все основные средства языка: типы данных, операторы, управляющие операторы, классы, интерфейсы, методы, делегаты, индексаторы, события, указатели, обобщения, коллекции, основные библиотеки классов, средства многопоточного программирования и директивы препроцессора.

В книге подробно описаны новые возможности C# 4.0, в том числе pLINQ (parallel LINQ), библиотека TPL (Task Parallel Library), динамический тип данных, а также именованные и необязательные аргументы.

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

На русском языке книга будет выпущена издательством "Диалектика-Вильямс" в 2011 году. Здесь мы представляем вам отрывки, касающиеся нововведений C# 4.0.

Ковариантность и контравариантность в параметрах обобщенного типа

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

Применение ковариантности в обобщенном интерфейсе

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

Для того чтобы стали понятнее последствия применения ковариантности, обратимся к конкретному примеру. Ниже приведен очень простой интерфейс IMyCoVarGenIF, в котором применяется ковариантность.

// В этом обобщенном интерфейсе поддерживается ковариантность.
public interface IMyCoVarGenIF<out T> {
  T GetObject();
} 

Обратите особое внимание на то, как объявляется параметр обобщенного типа T. Его имени предшествует ключевое слово out. В данном контексте ключевое слово out обозначает, что обобщенный тип T является ковариантным. А раз он ковариантный, то метод GetObject() может возвращать ссылку на обобщенный тип T или же ссылку на любой класс, производный от типа T.

Несмотря на свою ковариантность по отношению к обобщенному типу T, интерфейс IMyCoVarGenIF реализуется аналогично любому другому обобщенному интерфейсу. Ниже приведен пример реализации этого интерфейса в классе MyClass.

// Реализовать интерфейс IMyCoVarGenIF.
class MyClass<T> : IMyCoVarGenIF<T> {
  T obj;
  public MyClass(T v) { obj = v; }
  public T GetObject() { return obj; }
} 

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

А теперь рассмотрим следующую простую реализацию иерархии классов.

// Создать простую иерархию классов.
class Alpha 
{
  string name;

  public Alpha(string n) { name = n; }

  public string GetName() { return name; } 
    // ... 
} 
class Beta : Alpha 
{
  public Beta(string n) : base(n) { }
  // ...
} 

Как видите, класс Beta является производным от класса Alpha.

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

// Создать ссылку из интерфейса IMyCoVarGenIF на объект типа MyClass<Alpha>.
// Это вполне допустимо как при наличии ковариантности, так и без нее.
IMyCoVarGenIF<Alpha> AlphaRef =
  new MyClass<Alpha>(new Alpha("Alpha #1")); 

Console.WriteLine("Имя объекта, на который ссылается переменная AlphaRef: " + 
  AlphaRef.GetObject().GetName()); 
// А теперь создать объект MyClass<Beta> и присвоить его переменной AlphaRef.
// *** Эта строка кода вполне допустима благодаря ковариантности. ***
AlphaRef = new MyClass<Beta>(new Beta("Beta #1")); 

Console.WriteLine("Имя объекта, на который теперь ссылается " +
  "переменная AlphaRef: " + AlphaRef.GetObject().GetName()); 

Прежде всего, переменной AlphaRef типа IMyCoVarGenIF<Alpha>в этом фрагменте кода присваивается ссылка на объект типа MyClass<Alpha>. Это вполне допустимая операция, поскольку в классе MyClass реализуется интерфейс IMyCoVarGenIF, причем и в том, и в другом в качестве аргумента типа указывается Alpha. Далее имя объекта выводится на экран при вызове метода GetName() для объекта, возвращаемого методом GetObject(). И эта операция вполне допустима, поскольку Alpha — это и тип, возвращаемый методом Get­Name(), и обобщенный тип T. После этого переменной AlphaRef присваивается ссылка на экземпляр объекта типа MyClass<Beta>, что также допустимо, потому что класс Beta является производным от класса Alpha, а обобщенный тип T — ковариантным в интерфейсе IMyCoVarGenIF. Если бы любое из этих условий не выполнялось, данная операция оказалась бы недопустимой.

Ради большей наглядности примера вся рассмотренная выше последовательность операций собрана ниже в единую программу.

// Продемонстрировать ковариантность в обобщенном интерфейсе.
using System; 
// Этот обобщенный интерфейс поддерживает ковариантность.
public interface IMyCoVarGenIF<out T> 
{
  T GetObject();
} 
// Реализовать интерфейс IMyCoVarGenIF.class MyClass<T> : IMyCoVarGenIF<T> 
{
  T obj;
  public MyClass(T v) { obj = v; }
  public T GetObject() { return obj; }
} 
// Создать простую иерархию классов.
class Alpha 
{
  string name;
  public Alpha(string n) { name = n; } 
  public string GetName() { return name; }
  // ...
} 

class Beta : Alpha 
{
  public Beta(string n) : base(n) { }
  // ...
} 
class VarianceDemo 
{
  static void Main() 
  {
    // Создать ссылку из интерфейса IMyCoVarGenIF на объект типа MyClass<Alpha>.
    // Это вполне допустимо как при наличии ковариантности, так и без нее.
    IMyCoVarGenIF<Alpha> AlphaRef = new MyClass<Alpha>(new Alpha("Alpha #1"));

    Console.WriteLine("Имя объекта, на который ссылается переменная " + 
      "AlphaRef: " + AlphaRef.GetObject().GetName()); 
    // А теперь создать объект MyClass<Beta> и присвоить его 
    // переменной AlphaRef. 
    // *** Эта строка кода вполне допустима благодаря ковариантности. *** 
    AlphaRef = new MyClass<Beta>(new Beta("Beta #1")); 
    Console.WriteLine("Имя объекта, на который теперь ссылается переменная " 
      +"AlphaRef: " + AlphaRef.GetObject().GetName());
  }
} 

Результат выполнения этой программы выглядит следующим образом.

Имя объекта, на который ссылается переменная AlphaRef: Alpha #1
Имя объекта, на который теперь ссылается переменная AlphaRef: Beta #1 

Следует особо подчеркнуть, что переменной AlphaRef можно присвоить ссылку на объект типа MyClass<Beta> благодаря только тому, что обобщенный тип T указан как ковариантный в интерфейсе IMyCoVarGenIF. Для того чтобы убедиться в этом, удалите ключевое слово out из объявления параметра обобщенного типа T в интерфейсе IMyCoVarGenIF и попытайтесь скомпилировать данную программу еще раз. Компиляция завершится неудачно, поскольку строгая проверка на соответствие типов не разрешит теперь подобное присваивание.

Один обобщенный интерфейс может вполне наследовать от другого. Иными словами, обобщенный интерфейс с параметром ковариантного типа можно расширить, как показано ниже.

public interface IMyCoVarGenIF2<out T> : IMyCoVarGenIF<T> 
{
  // ...
} 

Обратите внимание на то, что ключевое слово out указано только в объявлении расширенного интерфейса. Указывать его в объявлении базового интерфейса не только не нужно, но и не допустимо. И последнее замечание: обобщенный тип T допускается не указывать как ковариантный в объявлении интерфейса IMyCoVarGenIF2. Но при этом исключается ковариантность, которую может обеспечить расширенный интерфейс IMyCoVarGetIF. Разумеется, возможность сделать интерфейс IMyCoVarGenIF2 инвариантным может потребоваться в некоторых случаях его применения.

На применение ковариантности накладываются некоторые ограничения. Ковариантность параметра типа может распространяться только на тип, возвращаемый методом. Следовательно, ключевое слово out нельзя применять в параметре типа, служащем для объявления параметра метода. Ковариантность оказывается пригодной только для ссылочных типов. Ковариантный тип нельзя использовать в качестве ограничения в интерфейсном методе. Так, следующий интерфейс считается недопустимым.

public interface IMyCoVarGenIF2<out T> 
{
  void M<V>() where V:T; 
  // Ошибка, ковариантный тип T нельзя
  // использовать как ограничение
} 

Применение контравариантности в обобщенном интерфейсе

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

Для того чтобы стали понятнее последствия применения ковариантности, вновь обратимся к конкретному примеру. Ниже приведен обобщенный интерфейс IMyContraVarGenIF контравариантного типа. В нем указывается контравариантный параметр обобщенного типа T, который используется в объявлении метода Show().

// Это обобщенный интерфейс, поддерживающий контравариантность.
public interface IMyContraVarGenIF<in T> 
{
  void Show(T obj);
} 

Как видите, обобщенный тип T указывается в данном интерфейсе как контравариантный с помощью ключевого слова in, предшествующего имени его параметра. Обратите также внимание на то, что T является параметром типа для аргумента obj в методе Show().

Далее интерфейс IMyContraVarGenIF реализуется в классе MyClass, как показано ниже.

// Реализовать интерфейс IMyContraVarGenIF.
class MyClass<T> : IMyContraVarGenIF<T> 
{
  public void Show(T x) { Console.WriteLine(x); }
} 

В данном случае метод Show() просто выводит на экран строковое представление переменной x, получаемое в результате неявного обращения к методу ToString() из метода WriteLine().

После этого объявляется иерархия классов, как показано ниже.

// Создать простую иерархию классов.
class Alpha 
{ 
  public override string ToString() 
  {
    return "Это объект класса Alpha."; 
  } 
  // ... 
} 
class Beta : Alpha 
{
  public override string ToString() 
  {
    return "Это объект класса Beta."; 
  } 
  // ... 
} 

Ради большей наглядности классы Alpha и Beta несколько отличаются от аналогичных классов из предыдущего примера применения ковариантности. Обратите также внимание на то, что метод ToString() переопределяется таким образом, чтобы возвращать тип объекта.

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

// Создать ссылку из интерфейса IMyContraVarGenIF<Alpha> 
// на объект типа MyClass<Alpha>. 
// Это вполне допустимо как при наличии контравариантности, так и без нее. 
IMyContraVarGenIF<Alpha> AlphaRef = new MyClass<Alpha>(); 

// Создать ссылку из интерфейса IMyContraVarGenIF<beta> 
// на объект типа MyClass<Beta>. 
// И это вполне допустимо как при наличии контравариантности, так и без нее. 
IMyContraVarGenIF<Beta> BetaRef = new MyClass<Beta>(); 

// Создать ссылку из интерфейса IMyContraVarGenIF<beta> 
// на объект типа MyClass<Alpha>. 
// *** Это вполне допустимо благодаря контравариантности. *** 
IMyContraVarGenIF<Beta> BetaRef2 = new MyClass<Alpha>(); 

// Этот вызов допустим как при наличии контравариантности, так и без нее. 
BetaRef.Show(new Beta()); 

// Присвоить переменную AlphaRef переменной BetaRef. 
// *** Это вполне допустимо благодаря контравариантности. *** 
BetaRef = AlphaRef;

BetaRef.Show(new Beta()); 

Прежде всего обратите внимание на создание двух переменных ссылочного типа IMyContraVarGenIF, которым присваиваются ссылки на объекты класса MyClass, где параметры типа совпадают с аналогичными параметрами в интерфейсных ссылках. В первом случае используется параметр типа Alpha, а во втором — параметр типа Beta. Эти объявления не требуют контравариантности и допустимы в любом случае.

Далее создается переменная ссылочного типа IMyContraVarGenIF<Beta>, но на этот раз ей присваивается ссылка на объект класса MyClass<Alpha>. Эта операция вполне допустима, поскольку обобщенный тип T объявлен как контравариантный.

Как и следовало ожидать, следующая строка, в которой вызывается метод BetaRef. Show() с аргументом Beta, является вполне допустимой. Ведь Beta — это обобщенный тип T в классе MyClass<Beta> и в то же время аргумент в методе Show().

В следующей строке переменная AlphaRef присваивается переменной BetaRef. Эта операция вполне допустима лишь в силу контравариантности. В данном случае переменная относится к типу MyClass<Beta>, а переменная AlphaRef — к типу MyClass<Alpha>. Но поскольку Alpha является базовым классом для класса Beta, то такое преобразование типов оказывается допустимым благодаря контравариантности. Для того чтобы убедиться в необходимости контравариантности в рассматриваемом здесь примере, попробуйте удалить ключевое слово in из объявления обобщенного типа T в интерфейсе IMyContraVarGenIF, а затем попытайтесь скомпилировать приведенный выше код еще раз. В результате появятся ошибки компиляции.

Ради большей наглядности примера вся рассмотренная выше последовательность операций собрана ниже в единую программу.

// Продемонстрировать контравариантность в обобщенном интерфейсе.
using System; 

// Это обобщенный интерфейс, поддерживающий контравариантность.
public interface IMyContraVarGenIF<in T> 
{
  void Show(T obj);
} 

// Реализовать интерфейс IMyContraVarGenIF.
class MyClass<T> : IMyContraVarGenIF<T> 
{
  public void Show(T x) { Console.WriteLine(x); }
} 

// Создать простую иерархию классов.
class Alpha 
{
  public override string ToString() 
  {
    return "Это объект класса Alpha.";
  }
  // ...
} 

class Beta : Alpha 
{
  public override string ToString() 
  {
    return "Это объект класса Beta.";
  }
  // ...
} 

class VarianceDemo 
{
  static void Main() 
  {
    // Создать ссылку из интерфейса IMyContraVarGenIF<Alpha>
    // на объект типа MyClass<Alpha>.
    // Это вполне допустимо как при наличии контравариантности, так и без нее.
    IMyContraVarGenIF<Alpha> AlphaRef = new MyClass<Alpha>();

    // Создать ссылку из интерфейса IMyContraVarGenIF<beta> 
    // на объект типа MyClass<Beta>. 
    // И это вполне допустимо как при наличии контравариантности, 
    // так и без нее.
    IMyContraVarGenIF<Beta> BetaRef = new MyClass<Beta>();

    // Создать ссылку из интерфейса IMyContraVarGenIF<beta>
    // на объект типа MyClass<Alpha>.
    // *** Это вполне допустимо благодаря контравариантности. ***
    IMyContraVarGenIF<Beta> BetaRef2 = new MyClass<Alpha>();

    // Этот вызов допустим как при наличии контравариантности, так и без нее.
    BetaRef.Show(new Beta());

    // Присвоить переменную AlphaRef переменной BetaRef.
    // *** Это вполне допустимо благодаря контравариантности. ***
    BetaRef = AlphaRef;

    BetaRef.Show(new Beta());
  }
} 

Выполнение этой программы дает следующий результат.

Это объект класса Beta. Это объект класса Beta. 

Контравариантный интерфейс может быть расширен аналогично описанному выше расширению ковариантного интерфейса. Для достижения контравариантного характера расширенного интерфейса в его объявлении должен быть указан такой же параметр обобщенного типа, как и у базового интерфейса, но с ключевым словом in, как показано ниже.

public interface IMyContraVarGenIF2<in T> : IMyContraVarGenIF<T> 
{ 
  // ... 
} 

Следует иметь в виду, что указывать ключевое слово in в объявлении базового интерфейса не только не нужно, но и не допустимо. Более того, сам расширенный интерфейс IMyContraVarGenIF2 не обязательно должен быть контравариантным. Иными словами, обобщенный тип T в интерфейсе IMyContraVarGenIF2 не требуется модифицировать ключевым словом in. Разумеется, все преимущества, которые сулит контравариантность в интерфейсе IMyContraVarGen, при этом будут утрачены в интерфейсе IMyContraVarGenIF2.

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

Вариантные делегаты

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

Ниже приведен пример контравариантного делегата.

// Объявить делегат, контравариантный по отношению к обобщенному типу T.
delegate bool SomeOp<in T>(T obj); 

Этому делегату можно присвоить метод с параметром обобщенного типа T или же класс, производный от типа T. А вот пример ковариантного делегата.

// Объявить делегат, ковариантный по отношению к обобщенному типу T.
delegate T AnotherOp<out T, V>(V obj); 

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

В следующем примере программы демонстрируется применение обоих разновидностей вариантных делегатов на практике.

// Продемонстрировать ковариантность и контравариантность
// в обобщенных делегатах. 

using System; 

// Объявить делегат, контравариантный по отношению к обобщенному типу T.
delegate bool SomeOp<in T>(T obj); 

// Объявить делегат, ковариантный по отношению к обобщенному типу T.
delegate T AnotherOp<out T, V>(V obj); 

class Alpha 
{
  public int Val { get; set; }

  public Alpha(int v) { Val = v; }
} 

class Beta : Alpha 
{
  public Beta(int v) : base(v) { }
} 

class GenDelegateVarianceDemo 
{
  // Возвратить логическое значение true, если значение
  // переменной obj.Val окажется четным.
  static bool IsEven(Alpha obj) 
  {
    if((obj.Val % 2) == 0) return true; 
    return false; 
  } 

  static Beta ChangeIt(Alpha obj) 
  { 
    return new Beta(obj.Val +2); 
  } 

  static void Main() 
  { 
    Alpha objA = new Alpha(4); 
    Beta objB = new Beta(9); 

    // Продемонстрировать сначала контравариантность. 
    // Объявить делегат SomeOp<Alpha> и задать для него метод IsEven. 
    SomeOp<Alpha> checkIt = IsEven; 
    // Объявить делегат SomeOp<Beta>. 
    SomeOp<Beta> checkIt2; 

    // А теперь присвоить делегат SomeOp<Alpha> делегату SomeOp<Beta>. 
    // *** Это допустимо только благодаря контравариантности. *** 
    checkIt2 = checkIt; 

    // Вызвать метод через делегат. 
    Console.WriteLine(checkIt2(objB)); 
    // Далее, продемонстрировать контравариантность.

    // Объявить сначала два делегата типа AnotherOp. 
    // Здесь возвращаемым типом является класс Beta, 
    // а параметром типа — класс Alpha. 
    // Обратите внимание на то, что для делегата modifyIt 
    // задается метод ChangeIt. 
    AnotherOp<Beta, Alpha> modifyIt = ChangeIt; 

    // Здесь возвращаемым типом является класс Alpha, 
    // а параметром типа — тот же класс Alpha. 
    AnotherOp<Alpha, Alpha> modifyIt2; 

    // А теперь присвоить делегат modifyIt делегату modifyIt2. 
    // *** Это допустимо только благодаря ковариантности. *** 
    modifyIt2 = modifyIt; 

    // Вызвать метод и вывести результаты на экран. 
    objA = modifyIt2(objA); 
    Console.WriteLine(objA.Val); 
  }
} 

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

False 
6 

Каждая операция достаточно подробно поясняется в комментариях к данной программе. Следует особо подчеркнуть, для успешной компиляции программы в объявлении обоих типов делегатов SomeOp и AnotherOp должны быть непременно указаны ключевые слова in и out соответственно. Без этих модификаторов компиляция программы будет выполнена с ошибками из-за отсутствия неявных преобразований типов в означенных строках кода.

Создание экземпляров объектов обобщенных типов

Когда приходится иметь дело с обобщениями, то нередко возникает вопрос: не приведет ли применение обобщенного класса к неоправданному раздуванию кода? Ответ на этот вопрос прост: не приведет. Дело в том, что в C# обобщения реализованы весьма эффективным образом: новые объекты конструируемого типа создаются лишь по мере надобности. Этот процесс описывается ниже.

Создание объектов динамического типа

C# является строго типизированным языком программирования. Вообще говоря, это означает, что все операции проверяются во время компиляции на соответствие типов, и поэтому действия, не поддерживаемые конкретным типом, не подлежат компиляции. И хотя строгий контроль типов дает немало преимуществ программирующему, помогая создавать устойчивые и надежные программы, он может вызвать определенные осложнения в тех случаях, когда тип объекта остается неизвестным вплоть до времени выполнения. Нечто подобное может произойти при использовании рефлексии, доступе к COM-объекту или же в том случае, если требуется возможность взаимодействия с таким динамическим языком, как, например, IronPython. До появления версии C# 4.0 подобные ситуации были трудноразрешимы. Поэтому для выхода из столь затруднительного положения в версии C# 4.0 был внедрен новый тип данных под названием dynamic.

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

В приведенном ниже примере программы применение типа dynamic демонстрируется на практике.

// Продемонстрировать применение типа dynamic. 

using System;
using System.Globalization; 

class DynDemo 
{
  static void Main() 
  {
    // Объявить две динамические переменные.
    dynamic str;
    dynamic val;
    // Поддерживается неявное преобразование в динамические типы. 
    // Поэтому следующие присваивания вполне допустимы. 
    str = "Это строка"; 
    val = 10; 

    Console.WriteLine("Переменная str содержит: " + str); 
    Console.WriteLine("Переменная val содержит: " + val + '\n'); 

    str = str.ToUpper(CultureInfo.CurrentCulture); 
    Console.WriteLine("Переменная str теперь содержит: " + str); 

    val = val + 2; 
    Console.WriteLine("Переменная val теперь содержит: " + val + '\n'); 

    string str2 = str.ToLower(CultureInfo.CurrentCulture); 
    Console.WriteLine("Переменная str2 содержит: " + str2); 

    // Поддерживаются неявные преобразования из динамических типов.
    int x = val * 2;
    Console.WriteLine("Переменная x содержит: " + x);
  }
} 

Выполнение этой программы дает следующий результат.

Переменная str содержит: Это строка 
Переменная val содержит: 10 

Переменная str теперь содержит: ЭТО СТРОКА 
Переменная val теперь содержит: 12 

Переменная str2 содержит: это строка 
Переменная x содержит: 24 

Обратите внимание в этой программе на две переменные str и val, объявляемые с помощью типа dynamic. Это означает, что проверка на соответствие типов операций с участием обеих переменных не будет произведена во время компиляции. В итоге для них оказывается пригодной любая операция. В данном случае для переменной str вызываются методы ToUpper() и ToLower() класса String, а переменная участвует в операциях сложения и умножения. И хотя все перечисленные выше действия совместимы с типами объектов, присваиваемых обеим переменным в рассматриваемом здесь примере, компилятору об этом ничего не известно — он просто принимает. И это, конечно, упрощает программирование динамических процедур, хотя и допускает возможность появления ошибок в подобных действиях во время выполнения.

В разбираемом здесь примере программа ведет себя “правильно” во время выполнения, поскольку объекты, присваиваемые упомянутым выше переменным, поддерживают действия, выполняемые в программе. В частности, переменной val присваивается целое значение, и поэтому она поддерживает такие целочисленные операции, как сложение. А переменной str присваивается символьная строка, и поэтому она поддерживает строковые операции. Следует, однако, иметь в виду, что ответственность за фактическую поддержку типом объекта, на который делается ссылка, всех операций над данными типа dynamic возлагается на самого программирующего. В противном случае выполнение программы завершится аварийным сбоем.

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

bool b = val; 

то возникнет ошибка при выполнении из-за отсутствия неявного преобразования типа int (который оказывается типом переменной val во время выполнения) в тип bool. Поэтому данная строка кода приведет к ошибке при выполнении, хотя она и будет скомпилирована безошибочно.

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

Для того чтобы стало понятно, насколько тип dynamic способен упростить решение некоторых задач, рассмотрим простой пример его применения вместе с рефлексией. Как пояснялось в главе 17, чтобы вызвать метод для объекта класса, получаемого во время выполнения с помощью рефлексии, можно, в частности, обратиться к методу Invoke(). И хотя такой способ оказывается вполне работоспособным, нужный метод намного удобнее вызвать по имени в тех случаях, когда его имя известно. Например, вполне возможна такая ситуация, когда в некоторой сборке содержится конкретный класс, поддерживающий методы, имена и действия которых заранее известны. Но поскольку эта сборка подвержена изменениям, то приходится постоянно убеждаться в том, что используется последняя ее версия. Для проверки текущей версии сборки можно, например, воспользоваться рефлексией, сконструировать объект искомого класса, а затем вызвать методы, определенные в этом классе. Теперь эти методы можно вызвать по имени с помощью типа dynamic, а не метода Invoke(), поскольку их имена известны.

Разместите сначала приведенный ниже код в файле с именем MyClass.cs. Этот код будет динамически загружаться посредством рефлексии.

public class DivBy 
{
  public bool IsDivBy(int a, int b) 
  {
    if((a % b) == 0) return true;
      return false
  ;}
  public bool IsEven(int a) 
  {
    if((a % 2) == 0) return true;
      return false;
  }
} 

Затем скомпилируйте этот файл в библиотеку DLL под именем MyClass.dll. Если вы пользуетесь компилятором командной строки, введите в командной строке следующее.

      csc /t:library MyClass.cs 
    

Далее составьте программу, в которой применяется библиотека MyClass.dll, как показано ниже.

// Использовать тип dynamic вместе с рефлексией. 

using System;
using System.Reflection; 

class DynRefDemo 
{
  static void Main() 
  {
    Assembly asm = Assembly.LoadFrom("MyClass.dll");

    Type[] all = asm.GetTypes();

    // Найти класс DivBy. 
    int i; 
    for(i = 0; i < all.Length; i++) 
      if(all[i].Name == "DivBy") break;

    if(i == all.Length) 
    { 
      Console.WriteLine("Класс DivBy не найден в сборке."); 
      return; 
    }

    Type t = all[i];

    // А теперь найти используемый по умолчанию конструктор. 
    ConstructorInfo[] ci = t.GetConstructors(); 

    int j; 
    for(j = 0; j < ci.Length; j++) 
      if(ci[j].GetParameters().Length == 0) break; 

    if(j == ci.Length) 
    {
      Console.WriteLine("Используемый по умолчанию конструктор не найден.");
      return;
    }

    // Создать объект класса DivBy динамически. 
    dynamic obj = ci[j].Invoke(null); 

    // Далее вызвать по имени методы для переменной obj. Это вполне допустимо,
    // поскольку переменная obj относится к типу dynamic, а вызовы методов
    // проверяются на соответствие типов во время выполнения, а не компиляции.
    if(obj.IsDivBy(15, 3))
      Console.WriteLine("15 делится нацело на 3."); 
    else 
      Console.WriteLine("15 НЕ делится нацело на 3."); 

      if(obj.IsEven(9))
        Console.WriteLine("9 четное число.");
      else 
        Console.WriteLine("9 НЕ четное число.");
  }
} 

Как видите, в данной программе сначала динамически загружается библиотека MyClass.dll, а затем используется рефлексия для построения объекта класса DivBy. Построенный объект присваивается далее переменной obj типа dynamic. А раз так, то методы IsDivBy() и IsEven()могут быть вызваны для переменной obj по имени, а не с помощью метода Invoke(). В данном примере это вполне допустимо, поскольку переменная obj на самом деле ссылается на объект класса DivBy. В противном случае выполнение программы завершилось бы неудачно.

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

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

static void f(object v) { // ... }
static void f(dynamic v) { // ... } // Ошибка! 

И последнее замечание: тип dynamic поддерживается компонентом DLR (Dynamic Language Runtime — средство создания динамических языков во время выполнения), внедренным в .NET 4.0.

Возможность взаимодействия с моделью COM

В версии C# 4.0 внедрены средства, упрощающие возможность взаимодействия с неуправляемым кодом, определяемым моделью компонентных объектов (COM) и применяемым, в частности, в COM-объекте Office Auto­mation. Некоторые из этих средств, в том числе тип dy­namic, именованные и необязательные свойства, пригодны для применения помимо возможности взаимодействия с моделью COM. Тема модели COM вообще и COM-объекта Office Automation в частности весьма обширна, а порой и довольно сложна, чтобы обсуждать ее в этой книге. Поэтому возможность взаимодействия с моделью COM выходит за рамки данной книги.

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

Как вам должно быть уже известно, в C# свойство обычно связывается только с одним значением с помощью одного из аксессоров get или set. Но совсем иначе дело обстоит со свойствами модели COM. Поэтому, начиная с версии C# 4.0, в качестве выхода из этого затруднительного положения во время работы с COM-объ­ектом появилась возможность пользоваться индексированным свойством для доступа к COM-свойству, имеющему несколько параметров. С этой целью имя свойства индексируется, почти так же, как это делается с помощью индексатора. Допустим, что имеется объект my­XLApp, который относится к типу Micro­soft.Of­fi­ce.In­terop.Execl.Application.

В прошлом для установки строкового значения "OK" в ячейках C1–C3 электронной таблицы Excel можно было бы воспользоваться оператором, аналогичным следующему.

myXLapp.get_Range("C1", "C3").set_Value(Type.Missing, "OK"); 

В этой строке кода интервал ячеек электронной таблицы получается при вызове метода get_Range(), для чего достаточно указать начало и конец интервала. А значения задаются при вызове метода set_Value(), для чего достаточно указать тип (что не обязательно) и конкретное значение. В этих методах используются свойства Range и Value, поскольку у обоих свойств имеются два параметра. Поэтому в прошлом к ним нельзя было обращаться как к свойствам, но приходилось пользоваться упомянутыми выше методами. Кроме того, аргумент Type.Missing служил в качестве обычного заполнителя, который передавался для указания на тип, используемый по умолчанию. Но, начиная с версии C# 4.0, появилась возможность переписать приведенный выше оператор, приведя его к следующей, более удобной, форме.

myXLapp.Range["C1", "C3"].Value = "OK"; 

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

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

Дружественные сборки

Одну сборку можно сделать дружественной по отношению к другой. Такой сборке доступны закрытые члены дружественной ей сборки. Благодаря этому средству становится возможным коллективное использование членов выбранных сборок, причем эти члены не нужно делать открытыми. Чтобы объявить дружественную сборку, необходимо воспользоваться атрибутом InternalsVisibleTo.

Исследование возможностей PLINQ

PLINQ представляет собой параллельный вариант языка интегрированных запросов LINQ и тесно связан с библиотекой TPL. PLINQ применяется, главным образом, для достижения параллелизма данных внутри запроса. Как станет ясно из дальнейшего, сделать это совсем не трудно. Как и TPL, тема PLINQ довольно обширна и многогранна, поэтому здесь представлены лишь самые основные понятия данного языка.

Класс ParallelEnumerable

Основу PLINQ составляет класс ParallelEnumerable, определенный в пространстве имен System.Linq. Это статический класс, в котором определены многие методы расширения, поддерживающие параллельное выполнение операций. По существу, он представляет собой параллельный вариант стандартного для LINQ класса Enumerable. Многие его методы являются расширением класса ParallelQuery, а некоторые из них возвращают объект типа ParallelQuery. В классе ParallelQuery инкапсулируется последовательность операций, поддерживающая параллельное выполнение. Имеются как обобщенный, так и необобщенный варианты данного класса. Мы не будем обращаться к классу ParallelQuery непосредственно, а воспользуемся несколькими методами класса ParallelEnumerable. Самый главный из них, метод AsParallel(), описывается в следующем разделе.

Распараллеливание запроса методом AsParallel()

Едва ли не самым удобным средством PLINQ является возможность просто создавать параллельный запрос. Нужно лишь вызвать метод AsParallel()для источника данных. Метод AsParallel()определен в классе ParallelEnumerable и возвращает источник данных, инкапсулированный в экземпляре объекта типа ParallelQuery. Это дает возможность поддерживать методы расширения параллельных запросов. После вызова данного метода запрос разделяет источник данных на части и оперирует с каждой из них таким образом, чтобы извлечь максимальную выгоду из распараллеливания. (Если распараллеливание оказывается невозможным или неприемлемым, то запрос, как обычно, выполняется последовательно.) Таким образом, добавления в исходный код единственного вызова метода AsParallel() оказывается достаточно для того, чтобы превратить последовательный запрос LINQ в параллельный запрос LINQ. Для простых запросов это единственное необходимое условие.

Существуют как обобщенные, так и необобщенные формы метода AsParallel(). Ниже приведена простейшая обобщенная его форма:

public static ParallelQuery AsParallel(this IEnumerable source)
public static ParallelQuery<TSource>
  AsParallel<TSource>(this IEnumerable<TSource> source) 

где TSource обозначает тип элементов в последовательном источнике данных source.

Ниже приведен пример, демонстрирующий простой запрос PLINQ.

// Простой запрос PLINQ. 
using System;
using System.Linq; 
  class PLINQDemo 
  {
    static void Main() 
    {
      int[] data = new int[10000000];

      // Инициализировать массив данных положительными значениями. 
      for(int i=0; i < data.Length; i++) data[i] = i; 

      // А теперь ввести в массив данных ряд отрицательных значений. 
      data[1000] = -1; 
      data[14000] = -2; 
      data[15000] = -3; 
      data[676000] = -4; 
      data[8024540] = -5;
      data[9908000] = -6;

      // Использовать запрос PLINQ для поиска отрицательных значений.
      var negatives = from val in data.AsParallel()
                      where val < 0
                      select val;

      foreach(var v in negatives) 
        Console.Write(v + " "); 
      Console.WriteLine();
    }
  } 

Эта программа начинается с создания крупного массива data, инициализируемого целыми положительными значениями. Затем в него вводится ряд отрицательных значений. А далее формируется запрос на возврат последовательности отрицательных значений. Ниже приведен этот запрос.

var negatives = from val in data.AsParallel()
                where val < 0
                select val; 

В этом запросе метод AsParallel()вызывается для источника данных, в качестве которого служит массив data. Благодаря этому разрешается параллельное выполнение операций над массивом data, а именно: поиск отрицательных значений параллельно в нескольких потоках. По мере обнаружения отрицательных значений они добавляются в последовательность вывода. Это означает, что порядок формирования последовательности вывода может и не отражать порядок расположения отрицательных значений в массиве data. В качестве примера ниже приведен результат выполнения приведенного выше кода в двухъядерной системе.

-5 -6 -1 -2 -3 -4 

Как видите, в том потоке, где поиск выполнялся в верхней части массива, отрицательные значения –5 и –6 были обнаружены раньше, чем значение -1 в том потоке, где поиск происходил в нижней части массива. Следует, однако, иметь в виду, что из-за отличий в степени загрузки задачами, количества доступных процессоров и прочих факторов системного характера могут быть получены разные результаты. А самое главное, что результирующая последовательность совсем не обязательно будет отражать порядок формирования исходной последовательности.

Применение метода AsOrdered()

Как отмечалось в предыдущем разделе, по умолчанию порядок формирования результирующей последовательности в параллельном запросе совсем не обязательно должен отражать порядок формирования исходной последовательности. Более того, результирующую последовательность следует рассматривать как практически неупорядоченную. Если же результат должен отражать порядок организации источника данных, то его нужно запросить специально с помощью метода AsOrdered(), определенного в классе ParallelEnumerable. Ниже приведены обобщенная и необобщенная формы этого метода:

public static ParallelQuery AsOrdered(this ParallelQuery source)
public static ParallelQuery<TSource>
  AsOrdered<TSource>(this ParallelQuery<TSource> source) 

где TSource обозначает тип элементов в источнике данных source. Метод AsOrdered() можно вызывать только для объекта типа ParallelQuery, поскольку он является методом расширения класса ParallelQuery.

Для того чтобы посмотреть, к какому результату может привести применение метода AsOrdered(), подставьте его вызов в приведенный ниже запрос из предыдущего примера программы.

// Использовать метод AsOrdered() для сохранения порядка 
// в результирующей последовательности. 
var negatives = from val in data.AsParallel().AsOrdered() 
                where val < 0
                select val; 

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

Отмена параллельного запроса

Параллельный запрос отменяется таким же образом, как и задача. И в том и в другом случае отмена опирается на структуру CancellationToken, получаемую из класса CancellationTokenSource. Получаемый в итоге признак отмены передается запросу с помощью метода WithCancellation(). Отмена параллельного запроса производится методом Cancel(), который вызывается для источника признаков отмены. Главное отличие отмены параллельного запроса от отмены задачи состоит в следующем: когда параллельный запрос отменяется, он генерирует исключение OperationCanceledException, а не AggregateException. Но в тех случаях, когда запрос способен сгенерировать несколько исключений, исключение OperationCanceledException может быть объединено в совокупное исключение AggregateException. Поэтому отслеживать лучше оба вида исключений.

Ниже приведена форма объявления метода With­Can­cellation():

public static ParallelQuery<TSource>
  WithCancellation<TSource> (
    this ParallelQuery<TSource> source,
    CancellationToken cancellationToken) 

где source обозначает вызывающий запрос, а can­cellationToken — признак отмены. Этот метод возвращает запрос, поддерживающий указанный признак отмены.

В приведенном ниже примере программы демонстрируется порядок отмены параллельного запроса, сформированного в программе из предыдущего примера. В данной программе организуется отдельная задача, которая ожидает в течение 100 миллисекунд, а затем отменяет запрос. Отдельная задача требуется потому, что цикл foreach, в котором выполняется запрос, блокирует выполнение метода Main() до завершения цикла.

// Отменить параллельный запрос. 
using System; 
using System.Linq;
using System.Threading;
using System.Threading.Tasks; 

class PLINQCancelDemo 
{
  static void Main() 
  { 
    CancellationTokenSource cancelTokSrc = new CancellationTokenSource(); 
    int[] data = new int[10000000]; 

    // Инициализировать массив данных положительными значениями. 
    for(int i=0; i < data.Length; i++) data[i] = i; 

    // А теперь ввести в массив данных ряд отрицательных значений. 
    data[1000] = -1; 
    data[14000] = -2; 
    data[15000] = -3; 
    data[676000] = -4; 
    data[8024540] = -5; 
    data[9908000] = -6; 

    // Использовать запрос PLINQ для поиска отрицательных значений. 
    var negatives = from val in data.AsParallel(). 
                      WithCancellation(cancelTokSrc.Token)
                    where val < 0 
                    select val;

    // Создать задачу для отмены запроса по истечении 100 миллисекунд.
    Task cancelTsk = Task.Factory.StartNew( () => {
                       Thread.Sleep(100); 
                       cancelTokSrc.Cancel(); 
                     });

    try { 
      foreach(var v in negatives) 
        Console.Write(v + " "); 
    } catch(OperationCanceledException exc) {
      Console.WriteLine(exc.Message);
    } catch(AggregateException exc) { 
      Console.WriteLine(exc); 
    } finally {
      cancelTsk.Wait(); 
      cancelTokSrc.Dispose(); 
      cancelTsk.Dispose(); 
    }
    Console.WriteLine();
  }
  } 

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

Запрос отменен с помощью маркера, переданного в метод WithCancellation.

Другие средства PLINQ

Как упоминалось ранее, PLINQ представляет собой довольно крупную подсистему. Это объясняется отчасти той гибкостью, которой обладает PLINQ. В PLINQ доступны и многие другие средства , помогающие подстраивать параллельные запросы под конкретную ситуацию. Так, при вызове метода WithDegreeOf­Pa­ral­le­lism() можно указать максимальное количество процессоров, выделяемых для обработки запроса, а при вызове метода AsSequential() — запросить последовательное выполнение части параллельного запроса. Если вызывающий поток, ожидающий результатов от цикла foreach, не требуется блокировать, то для этой цели можно воспользоваться методом ForAll(). Все эти методы определены в классе ParallelEnumerable. А в тех случаях, когда PLINQ должен по умолчанию поддерживать последовательное выполнение, можно воспользоваться методом WithExecutionMode(), передав ему в качестве параметра признак ParallelExecutionMode.Force­Paralle­lism.

Вопросы эффективности PLINQ

Далеко не все запросы выполняются быстрее только потому, что они распараллелены. Как пояснялось ранее в отношении TPL, издержки, связанные с созданием параллельных потоков и управлением их исполнением, могут “перекрыть” все преимущества, которые дает распараллеливание. Вообще говоря, если источник данных оказывается довольно мелким, а требующаяся обработка данных — очень короткой, то внедрение параллелизма может и не привести к ускорению обработки запроса . Поэтому за рекомендациями по данному вопросу следует обращаться к информации корпорации Microsoft.



Ваши предложения и комментарии мы ожидаем по адресу: mag@rsdn.ru
Copyright © 1994-2002 Оптим.ру