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


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

Обобщенная ковариантность и контравариантность в Visual Basic 2010

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

В Visual Studio 2010 появилась новая функциональность — обобщенная ковариантность и контравариантность (generic co- and contravariance), — доступная при работе с обобщенными интерфейсами и делегатами. В версиях до Visual Studio 2010 и Microsoft .NET Framework 4 обобщения вели себя инвариантно по отношению к подтипам (subtyping), поэтому преобразования между обобщенными типами с разными аргументами-типами не разрешались.

Например, если бы вы попытались передать List(Of Derived) в метод, принимающий IEnumerable(Of Base), то получили бы ошибку. Но Visual Studio 2010 умеет обрабатывать безопасные в многопоточной среде ковариантность и контравариантность, которые поддерживают объявление ковариантных и контравариантных параметров-типов в обобщенных интерфейсах и типах-делегатах. В этой статье мы обсудим, в чем смысл этой функциональности и как с выгодой использовать ее в приложениях.

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

Dim btnCollection As IEnumerable(Of Button) = New List(Of Button) From {New Button}
Dim ctrlCollection As IEnumerable(Of Control) = btnCollection

Однако в Visual Studio 2008 это не разрешается, и вы получите ошибку «IEnumerable(Of Button) cannot be converted to IEnumerable(Of Control)» [IEnumerable(Of Button) нельзя преобразовать в IEnumerable(Of Control)]. Но как объектно-ориентированные программисты мы знаем, что, согласно базовым принципам наследования, значение типа Button может быть преобразовано в тип Control, а значит, этот код обязан поддерживаться.

Рассмотрим следующий пример:

Dim btnCollection As IList(Of Button) = New List(Of Button) From {New Button}
Dim ctrlCollection As IList(Of Control) = btnCollection
ctrlCollection(0) = New Label
Dim firstButton As Button = btnCollection(0)

Этот код приведет к исключению InvalidCastException, потому что программист преобразовал IList(Of Button) в IList(Of Control), а затем вставил в него элемент управления, который даже не был кнопкой.

Visual Studio 2010 распознает и разрешает код, приведенный в первом примере, но по-прежнему запрещает выполнение кода из второго примера, если он рассчитан на .NET Framework 4. Подавляющая часть программ продолжит корректно работать, так что копаться в них не придется. Но в этой статье я хочу подробно объяснить, как и почему работает приведенный ранее код.

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

Почему в первом листинге, где IEnumerable(Of Button) представлен как IEnumerable(Of Control), код считается безопасным в Visual Studio 2010, а во втором, где IList(Of Button) представлен как IList(Of Control), — нет?

С первым листингом все в порядке, потому что IEnumerable(Of T) — «выходной» интерфейс, т. е. в IEnumerable(Of Control) подразумевается, что пользователи этого интерфейса могут лишь извлекать элементы управления из списка.

Второй листинг небезопасен из-за того, что IList(Of T) является «входным и выходным» интерфейсом, поэтому в IList(Of Control) пользователи интерфейса могут не только извлекать элементы управления из списка, но и помещать их туда.

Новое языковое средство в Visual Studio 2010, которое разрешает делать это, называют обобщенной ковариантностью (generic covariance). В .NET Framework 4 инфраструктура вокруг следующих строк была переписана:

Interface IEnumerable(Of Out T)
...
End Interface
Interface IList(Of T)
...
End Interface

Аннотация Out в IEnumerable(Of Out T) указывает, что если в каком-либо методе интерфейса IEnumerable упоминается T, оно встречается лишь в выходной (out) позиции, например для возврата функции или типа свойства только для чтения. Это позволяет приводить любой IEnumerable(Of Derived) к IEnumerable(Of Base) без генерации исключения InvalidCastException.

В IList аннотации нет, потому что IList(Of T) является и входным, и выходным интерфейсом. В итоге пользователям запрещается преобразование IList(Of Derived) в IList(Of Base) или наоборот; в ином случае вы получили бы InvalidCastException.

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

Существует зеркальный вариант аннотации Out. Эта штука потоньше, поэтому начну с примера:

Dim _compOne As IComparer(Of Control) = New MyComparerByControlName()
Dim _compTwo As IComparer(Of Button) = _compOne
Dim btnOne = new Button with {.Name = "btnOne", OnClick = AddressOf btnOneClick, Left=20}
Dim btnTwo = new Button with {.Name = "btnTwo", OnClick = AddressOf btnTwoClick, Left=100}
Dim areSame = _compTwo.Compare(btnOne, btnTwo)

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

Поскольку компаратор может сравнивать любые элементы управления, он, разумеется, позволяет распознать, что два элемента управления являются кнопками. Вот почему вы можете безопасно преобразовывать его в IComparer(Of Button). И вообще IComparer(Of Base) можно безопасно преобразовывать в любой IComparer(Of Derived). Это называют контравариантностью. При этом используется аннотация In, и она является точным зеркальным отражением аннотации Out.

.NET Framework 4 также была модифицирована для включения обобщенный параметров-типов In:

Interface IComparer(Of In T)
 ...
End Interface

Благодаря аннотации In в IComparer(Of T) в любом методе интерфейса IComparer, где упоминается T, оно будет встречаться лишь во входной позиции, например в аргументе ByVal или типе свойства только для записи. Таким образом, пользователи могут приводить IComparer(Of Base) к любому IComparer(Of Derived) без генерации исключения InvalidCastException.

Рассмотрим пример делегата Action в .NET 4, где делегат становится контравариантным в T:

Dim actionControl As Action(Of Control)
Dim actionButton As Action(Of Button) = actionControl

Этот пример работает в .NET 4, потому что пользователь делегата actionButton будет всегда вызывать его с аргументами Button, а это элементы управления.

Вы можете добавлять аннотации In и Out к собственным обобщенным интерфейсам и делегатам. Но из-за ограничений общеязыковой исполняющей среды (CLR) эти аннотации неприменимы к классам, структурам или каким-то другим конструкциям. То есть лишь интерфейсы и делегаты могут быть ко- или контравариантными.

Объявления и синтаксис

Visual Basic использует два новых контекстных ключевых слова: Out (ковариантность) и In (контравариантность). Эти ключевые слова демонстрируются в следующем примере:

Public Delegate Function Func(Of In TArg, Out TResult)(ByVal arg As TArg) As TResult

Public Interface IEnumerable(Of Out Tout)

  Inherits IEnumerable
  Function GetEnumerator() As IEnumerator(Of Tout)
End Interface

Public Interface IEnumerator(Of Out Tout)
  Inherits IEnumerator
  Function Current() As Tout
End Interface

Public Interface IComparer(Of In Tin)
  Function Compare(ByVal left As Tin, ByVal right As Tin) As Integer
End Interface

Но зачем нам эти два ключевых слова и вообще такой синтаксис? Почему вариантность In/Out не определяется автоматически? Во-первых, программистам всегда полезно явно объявлять о своих намерениях. Во-вторых, в некоторых местах компилятор не может самостоятельно определить наиболее подходящий вид вариантности.

Вот два интерфейса: IReadWriteBase и IReadWrite:

Interface IReadWriteBase(Of U)
  Function ReadWrite() As IReadWrite(Of U)
End Interface
Interface IReadWrite(Of T) : Inherits IReadWriteBase(Of T)
End Interface

Если компилятор логически определяет, что они оба относятся к Out, то код работает нормально:

Interface IReadWriteBase(Of Out U)
  Function ReadWrite() As IReadWrite(Of U)
End Interface
Interface IReadWrite(Of Out T)
  Inherits IReadWriteBase(Of T)
End Interface

Если компилятор считает, что они оба относятся к In, то код вновь работает нормально:

Interface IReadWrite(Of In T)
  Inherits IReadWriteBase(Of T)
End Interface
Interface IReadWriteBase(Of In U)
  Function ReadWrite() As IReadWrite(Of U)
End Interface

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

Итак, контекстуальные ключевые слова Out и In появляются только в объявлениях интерфейсов и делегатов. Применение этих ключевых слов в любом другом объявлении вызовет ошибку на этапе компиляции. Компилятор Visual Basic не разрешает включение в вариантные интерфейсы вложенных перечислений, классов и структур, так как CLR не поддерживает вариантные классы. Однако вы можете включать вариантные интерфейсы в любые классы.

Устранение неоднозначности

Ко- и контравариантность вводит вероятность неоднозначности, поэтому вы должны знать, что приводит к неоднозначности и как компилятор Visual Basic ведет себя в таких случаях.

Рассмотрим пример на рис. 1, в котором мы пытаемся преобразовать Comparer в IComparer(Of Control), где Comparer реализует IComparer(Of Button) и IComparer(Of CheckBox).

Рис. Неоднозначное преобразование

Option Strict On
Imports System.Windows.Forms
Interface IComparer(Of Out Tout)
End Interface
Class Comparer
    Implements IComparer(Of Button)
    Implements IComparer(Of CheckBox)
End Class

Module VarianceExample
    Sub Main()
        Dim iComp As IComparer(Of Control) = New Comparer()
    End Sub
End Module

Поскольку и IComparer(Of Button), и IComparer(Of CheckBox) являются «вариантно-преобразуемыми» в IComparer(Of Control), преобразование будет неоднозначным. В итоге компилятор Visual Basic будет искать неоднозначности по правилам CLR и, если Option Strict установлено в On, запретит любые неоднозначные преобразования на этапе компиляции; если же Option Strict установлено в Off, компилятор просто выдаст предупреждение.

Преобразование на рис. 2 пройдет успешно в период выполнения и не приведет к ошибке на этапе компиляции.

Рис. Преобразование, которое успешно проходит в период выполнения

Option Strict On
Imports System.Windows.Forms
Interface IEnumerable(Of Out Tout)
End Interface
Class ControlList
   Implements IEnumerable(Of Button)
   Implements IEnumerable(Of CheckBox)
End Class

Module VarianceExample
    Sub Main()
     Dim _ctrlList As IEnumerable(Of Control) = CType(New ControlList, IEnumerable(Of Button))
     Dim _ctrlList2 As IEnumerable(Of Control) = CType(New ControlList, IEnumerable(Of CheckBox))
    End Sub
End Module

Опасность реализации обобщенных интерфейсов IComparer(of IBase) и IComparer(of IDerived) в одном коде показана на рис. 3. Здесь классы Comparer1 и Comparer2 реализуют один и тот же вариантный обобщенный интерфейс с разными обобщенными параметрами-типами в разном порядке. Хотя Comparer1 и Comparer2 идентичны с тем исключением, что реализуют интерфейс с разным порядком параметров-типов, вызов метода Compare этих классов будет давать разные результаты.

Рис. 3 Другие результаты при использовании того же метода

Option Strict Off
Module VarianceExample
    Sub Main()
        Dim _comp As IComparer(Of Account) = New Comparer1()
        Dim _comp2 As IComparer(Of Account) = New Comparer2()

        Dim _account = New Account With {.AccountType = "Checking", .IsActive = True}
        Dim _account2 = New Account With {.AccountType = "Saving", .IsActive = False}

        ‘// Even though _comp and _comp2 are *IDENTICAL*, they give different results!
        Console.WriteLine(_comp.Compare(_account, _account2)) ‘; // prints 0
        Console.WriteLine(_comp2.Compare(_account, _account2)) ‘; // prints -1

    End Sub
    Interface IAccountRoot
        Property AccountType As String
    End Interface
    Interface IAccount
        Inherits IAccountRoot
        Property IsActive As Boolean
    End Interface
    Class Account
        Implements IAccountRoot, IAccount
        Public Property AccountType As String Implements IAccountRoot.AccountType
        Public Property IsActive As Boolean Implements IAccount.IsActive
    End Class

    Class Comparer1
        Implements IComparer(Of IAccountRoot), IComparer(Of IAccount)

        Public Function Compare(ByVal x As IAccount, ByVal y As IAccount) As Integer Implements System.Collections.Generic.IComparer(Of IAccount).Compare
            Dim c As Integer = String.Compare(x.AccountType, y.AccountType)
            If (c <> 0) Then
                Return c
            Else
                Return (If(x.IsActive, 0, 1)) - (If(y.IsActive, 0, 1))
            End If

        End Function

        Public Function Compare(ByVal x As IAccountRoot, ByVal y As IAccountRoot) As Integer Implements System.Collections.Generic.IComparer(Of IAccountRoot).Compare
            Return String.Compare(x.AccountType, y.AccountType)
        End Function
    End Class

    Class Comparer2
        Implements IComparer(Of IAccount), IComparer(Of IAccountRoot)

        Public Function Compare(ByVal x As IAccount, ByVal y As IAccount) As Integer Implements System.Collections.Generic.IComparer(Of IAccount).Compare
            Dim c As Integer = String.Compare(x.AccountType, y.AccountType)
            If (c <> 0) Then
                Return c
            Else
                Return (If(x.IsActive, 0, 1)) - (If(y.IsActive, 0, 1))
            End If
        End Function

        Public Function Compare(ByVal x As IAccountRoot, ByVal y As IAccountRoot) As Integer Implements System.Collections.Generic.IComparer(Of IAccountRoot).Compare
            Return String.Compare(x.AccountType, y.AccountType)
        End Function
    End Class
End Module

Почему код на рис. 3 дает разные результаты, хотя _comp и _comp2 идентичны? Компилятор просто генерирует MSIL-код (Microsoft Intermediate Language), который и выполняет преобразование. А значит, выбор интерфейса IComparer(Of IAccountRoot) или IComparer(Of IAccount) падает на CLR, которая всегда выбирает из списка интерфейсов первый совместимый. В данном случае метод Compare() дает разные результаты из-за того, что CLR выбирает интерфейс IComparer(Of IAccountRoot) для класса Comparer1 и интерфейс IComparer(Of IAccount) для класса Comparer2.

Ограничения для обобщенного интерфейса

Когда вы пишете ограничение для обобщенного интерфейса вроде (Of T As U, U), ключевое слово As, помимо наследования, теперь охватывает возможность вариантного преобразования (рис. 4).

Рис. 4 Ограничение для обобщенного интерфейса охватывает возможность вариантного преобразования

Option Strict On
Imports System.Windows.Forms
Module VarianceExample
    Interface IEnumerable(Of Out Tout)
    End Interface
    Class List(Of T)
        Implements IEnumerable(Of T)
    End Class
    Class Program
        Shared Function Foo(Of T As U, U)(ByVal arg As T) As U
            Return arg
        End Function

        Shared Sub Main()
            ‘This is allowed because it satisfies the constraint Button AS Control
            Dim _ctrl As Control = Foo(Of Button, Control)(New Button)
            Dim _btnList As IEnumerable(Of Button) = New List(Of Button)()
            ‘This is allowed because it satisfies the constraint IEnumerable(Of Button) AS IEnumerable(Of Control)
            Dim _ctrlCol As IEnumerable(Of Control) = Foo(Of IEnumerable(Of Button), IEnumerable(Of Control))(_btnList)

        End Sub
    End Class
End Module

Вариантный обобщенный параметр может быть ограничен другим вариантным параметром, например: Информация к размышлению:

Interface IEnumerable(Of In Tin, Out Tout As Tin)
End Interface
Interface IEnumerable(Of Out Tout, In Tin As Tout)
End Interface

В этом примере IEnumerator(Of ButtonBase, ButtonBase) можно вариантно преобразовывать в IEnumerator(Of Control, Button), а IEnumerable(Of Control, Button) разрешается преобразовывать в IEnumerable(Of ButtonBase, ButtonBase) при соблюдении ограничений. В принципе, вариантное преобразование можно было бы распространить и на IEnumerable(Of ButtonBase, Control), но оно уже не удовлетворяет ограничениям, а значит, не является допустимым типом. На рис. 5 представлен FIFO-набор объектов, где ограничение может быть полезно.

Рис. 5 Где ограничение может быть полезно в наборе объектов

Option Strict On
Imports System.Windows.Forms

Interface IPipe(Of Out Tout, In Tin As Tout)
    Sub Push(ByVal x As Tin)
    Function Pop() As Tout
End Interface

Class Pipe(Of T)
    Implements IPipe(Of T, T)

    Private m_data As Queue(Of T)

    Public Function Pop() As T Implements IPipe(Of T, T).Pop
        Return m_data.Dequeue()
    End Function

    Public Sub Push(ByVal x As T) Implements IPipe(Of T, T).Push
        m_data.Enqueue(x)
    End Sub
End Class

Module VarianceDemo
    Sub Main()
        Dim _pipe As New Pipe(Of ButtonBase)
        Dim _IPipe As IPipe(Of Control, Button) = _pipe
    End Sub
End Module

На рис. 5, если я передам вам _IPipe, вы сможете лишь вставить Button в конвейер, а из него — только считывать Control. Заметьте, что вы можете ограничить вариантный интерфейс до значимого типа (value type) при условии, что интерфейс не допускает вариантного преобразования. Вот пример с ограничением до значимого типа в обобщенном параметре:

Interface IEnumerable(Of Out Tout As Structure)
End Interface

Ограничения до значимых типов бесполезны, если вариантное преобразование не разрешено в вариантном интерфейсе, экземпляр которого создан с использованием значимых типов. Но, если Tout является структурой, это может оказаться полезным для логического распознавания типа по ограничениям.

Ограничения для обобщенных параметров функции

Ограничения в методах/функциях должны иметь In-типы по двум основным причинам:

  • в большинстве случаев обобщенный параметр функции является входным для нее, а все входные типы должны быть In-типов;
  • клиент может ковариантно преобразовывать любой Out-тип в System.Object. Если бы обобщенный параметр был ограничен каким-либо Out-типом, клиент мог бы удалить это ограничение, а ограничения предназначены вовсе не для этого.

Рассмотрим рис. 6, из которого станет ясно, что произошло бы без такого правила проверки на допустимость вариантности в ограничениях.

Рис 6 Что происходит без правила проверки на допустимость вариантности в ограничениях

Option Strict On
Imports System.Windows.Forms
Interface IEnumerable(Of Out Tout)
    Sub Foo(Of U As Tout)(ByVal arg As U)
End Interface

Class List(Of T)
    Implements IEnumerable(Of T)

    Private m_data As T
    Public Sub Foo(Of U As T)(ByVal arg As U) Implements IEnumerable(Of T).Foo
        m_data = arg
    End Sub
End Class

Module VarianceExample
    Sub Main()
        ‘Inheritance/Implements
        Dim _btnCollection As IEnumerable(Of Button) = New List(Of Button)
        ‘Covariance
        Dim _ctrlCollection As IEnumerable(Of Control) = _btnCollection
        ‘Ok, Constraint-satisfaction, because Label is a Control
        _ctrlCollection.Foo(Of Label)(New Label)
    End Sub
End Module

На рис. 6 мы сохранили Label в m_data как Button, что недопустимо. Так что ограничения в методах/функциях должны иметь In-типы.

Разрешение перегрузки

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

Давайте взглянем на следующий пример:

Private Overloads Sub Foo(ByVal arg As Integer)
End Sub
Private Overloads Sub Foo(ByVal arg As String)
End Sub

Foo(2)

Что здесь на самом деле происходит? Когда компилятор видит вызов Foo(2), он должен определить, какую Foo вы хотите вызвать. Для этого он использует следующий алгоритм.

  1. Генерируется набор всех применимых кандидатов поиском всего, что имеет имя Foo. В нашем примере таких кандидатов два.
  2. Для каждого кандидата просматривается список аргументов и отсеиваются неприменимые функции. Заметьте, что компилятор также выполняет кое-какую проверку и логически распознает типы для обобщений.

С введением вариантности набор предопределенных преобразований расширен, и в результате на этапе 2 к рассмотрению принимается большее число функций-кандидатов, чем раньше. Кроме того, раньше, когда встречались два равноценных кандидата, компилятор выбирал нетеневой экземпляр (unshadowed), но теперь теневой может быть шире, поэтому компилятор может выбрать его. На рис. 7 демонстрируется код, который потенциально может перестать работать с введением вариантности в Visual Basic.

Рис. 7 Код, который потенциально может перестать работать с введением вариантности в Visual Basic

Option Strict On
Imports System.Windows.Forms
Imports System.Collections.Generic
Module VarianceExample
    Sub Main()
        Dim _ctrlList = New ControlList(Of Button)
        ‘Picks Add(ByVal f As IEnumerable(Of Control)), Because of variance-convertibility
        _ctrlList.Add(New ControlList(Of Button))
    End Sub
End Module
Interface IEnumerable(Of Tout)
End Interface
Class ControlList(Of T)
    Implements IEnumerable(Of T)

    Sub Add(ByVal arg As Object)
    End Sub

    Sub Add(ByVal arg As IEnumerable(Of Control))
    End Sub
End Class

В Visual Studio 2008 вызов Add был бы связан с Object, но в Visual Studio 2010 из-за поддержки вариантных преобразований вместо этого используется IEnumerable(Of Control).

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

Методы расширения

Методы расширения позволяют добавлять методы к существующим типам без создания новых производных типов, перекомпиляции или иной модификации исходных типов. В Visual Studio 2008 методы расширения поддерживали ковариантность массивов, как показано ниже:

Option Strict On
Imports System.Windows.Forms
Imports System.Runtime.CompilerServices
Module VarianceExample
  Sub Main()
    Dim _extAdd(3) As Button ‘Derived from Control
    _extAdd.Add()
  End Sub

  <Extension()>
  Public Sub Add(ByVal arg() As Control)
     System.Console.WriteLine(arg.Length)
  End Sub

Но в Visual Studio 2010 методы расширения также затрагивают вариантность обобщений. Это может стать разрушительным изменением, как показано на рис. 8, потому что у вас может появиться больше кандидатов расширения, чем раньше.

Рис. 8 Разрушительное изменение

Option Strict On
Imports System.Runtime.CompilerServices
Imports System.Windows.Forms
Module VarianceExample
    Sub Main()
        Dim _func As Func(Of Button) = Function() New Button
        ‘This was a compile-time error in VB9, But in VB10 because of variance convertibility, the compiler uses the extension method.
        _func.Add()
    End Sub

    <Extension()> _
    Public Sub Add(ByVal this As Func(Of Control))
        Console.WriteLine(“A call to func of Control”)
    End Sub
End Module

Пользовательские преобразования

Visual Basic позволяет объявлять преобразования для классов и структур, чтобы их можно было преобразовывать в другие классы и структуры, а также в базовые типы. В Visual Studio 2010 поддержка вариантных преобразований уже добавлена в алгоритмы пользовательских преобразований. А значит, область видимости каждого пользовательского преобразования будет автоматически увеличиваться, что может вызвать нарушения в работе программ.

Поскольку Visual Basic и C# не поддерживают пользовательские преобразования для интерфейсов, нам придется беспокоиться только о типах-делегатах. Посмотрите на преобразование на рис. 9, которое работает в Visual Studio 2008, но дает ошибку в Visual Studio 2010.

Рис. 9 Преобразование, которое работает в Visual Studio 2008, но дает ошибку в Visual Studio 2010

Option Strict On
Imports System.Windows.Forms
Module VarianceExample
    Class ControlList
        Overloads Shared Widening Operator CType(ByVal arg As ControlList) As Func(Of Control)
            Console.WriteLine("T1->Func(Of Control)")
            Return Function() New Control
        End Operator
    End Class
    Class ButtonList
        Inherits ControlList
        Overloads Shared Widening Operator CType(ByVal arg As ButtonList) As Func(Of Button)
            Console.WriteLine("T2->Func(Of Button)")
            Return Function() New Button
        End Operator
    End Class
    Sub Main()
       'The conversion works in VB9 using ButtonList->ControlList->Func(Of Control)
'Variance ambiguity error in VB10, because there will be another widening path    (ButtonList-->Func(Of Button)--[Covariance]-->Func(Of Control)
        Dim _func As Func(Of Control) = New ButtonList
    End Sub
End Module

На рис. 10 приведен другой пример преобразования, которое вызовет ошибку компиляции в Visual Studio 2008 с Option Strict On, но будет успешно выполнено в Visual Studio 2010 с поддержкой вариантных преобразований.

Рис. 10 Visual Studio 2010 позволяет использование ранее недопустимого вариантного преобразования

Option Strict On
Imports System.Windows.Forms
Module VarianceExample
    Class ControlList
        Overloads Shared Narrowing Operator CType(ByVal arg As ControlList) As Func(Of Control)
            Return Function() New Control
        End Operator

        Overloads Shared Widening Operator CType(ByVal arg As ControlList) As Func(Of Button)
            Return Function() New Button
        End Operator
    End Class

    Sub Main()
‘This was an error in VB9 with Option Strict On, but the conversion will succeed in VB10 using Variance->Func(Of Button)-[Covariance]-Func(Of Control)
        Dim _func As Func(Of Control) = New ControlList
    End Sub
End Module

Влияние Option Strict Off

Option Strict Off обычно позволяет сужать неявно выполняемые преобразования. Но независимо от того, включен (On) или выключен (Off) Option Strict, функциональность вариантных преобразований требует, чтобы соответствующие обобщенные аргументы были связаны через CLR-расширение, совместимое с присваиванием (CLR's assignment-compatible widening); для них недостаточно связи через сужение (narrowing) (рис. 11). Заметьте: Мы считаем T->U сужающемся, если присутствует вариантное преобразование U->T, а также T->U сужающимся, если T->U неоднозначно.

Рис. 11 При Option Strict в состоянии выключен

Option Strict Off
Imports System.Windows.Forms
Module VarianceExample
    Interface IEnumerable(Of Out Tout)
    End Interface
    Class ControlList(Of T)
       Implements IEnumerable(Of T)
    End Class
    Sub Main()
        ‘No compile time error, but will throw Invalid Cast Exception at run time
        Dim _ctrlList As IEnumerable(Of Button) = New ControlList(Of Control)
    End Sub
End Module

Ограничения ко- и контравариантности

Вот список таких ограничений.

  1. Контекстуальные ключевые слова In и Out могут появляться в объявлениях интерфейсов или делегатов. Применение этих ключевых слов в любом другом объявлении дает ошибку компиляции. В вариантный интерфейс нельзя вкладывать класс или структуру, но он может содержать вложенные интерфейсы и делегаты, которые в таком случае унаследуют вариантность от содержащего их типа.
  2. Только интерфейсы и делегаты могут быть ковариантными или контравариантными и только в том случае, когда аргументы-типы являются ссылочными типами (reference types).
  3. Вариантное преобразование нельзя выполнять над вариантными интерфейсами, если их экземпляры были созданы с использованием значимых типов (value types).
  4. Перечисления, классы, события и структуры нельзя включать в вариантный интерфейс. Дело в том, что мы создаем эти классы/структуры/перечисления как обобщенные, наследуя обобщенные параметры их контейнеров, поэтому в итоге они унаследуют вариантность своих контейнеров. А вариантность классов/структур/перечислений запрещена спецификацией CLI.

Более гибкий и ясный код

При работе с обобщениями в некоторых случаях было понятно, что можно было бы написать более простой или четкий код, если бы поддерживалась ко- и контравариантность. Теперь, когда нужные средства реализованы в Visual Studio 2010 и .NET Framework 4, вы можете сделать свой код намного яснее и гибче, объявляя вариантные свойства параметров-типов в обобщенных интерфейсах и делегатах.

Чтобы облегчить эту задачу, в .NET Framework 4 интерфейс IEnumerable теперь объявляется ковариантным с модификатором Out в своем параметре-типе, а IComparer — контравариантным с модификатором In. Поэтому для вариантного преобразования IEnumerable(Of T) в IEnumerable(Of U) нужно соблюсти одно из следующих условий:

  • T наследует от U;
  • T является вариантно-преобразуемым в U;
  • T имеет любой другой вид предопределенного в CLR ссылочного преобразования (reference conversion).

В Basic Class Library эти интерфейсы объявляются так:

Interface IEnumerable(Of Out T)
  Function GetEnumerator() As IEnumerator(Of T)
End Interface
Interface IEnumerator(Of Out T)
  Function Current() As T
End Interface
Interface IComparer(Of In T)
  Function Compare(ByVal arg As T, ByVal arg2 As T) As Integer
End Interface
Interface IComparable(Of In T)
  Function CompareTo(ByVal other As T) As Integer
End Interface

Для безопасности в многопоточной среде ковариантные параметры-типы могут быть лишь возвращаемыми типами или свойствами только для чтения (например, как в методе GetEnumerator и свойстве Current, показанных выше); контравариантные параметры-типы могут быть лишь параметрами или свойствами только для записи (например, аргументами-типами как в методах Compare и CompareTo, показанных выше).

Ковариантность и контравариантность — интересные средства, избавляющие от некоторой негибкости при работе с обобщенными интерфейсами и делегатами. Иметь базовое представление об этих средствах очень полезно при написании кода, работающего с обобщениями в Visual Studio 2010.

Автор: Биньям Келиль  •  Иcточник: Журнал MSDN  •  Опубликована: 21.01.2011
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER


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