|
|
|||||||||||||||||||||||||||||
|
Обобщенная ковариантность и контравариантность в Visual Basic 2010Источник: Журнал MSDN Биньям Келиль
В Visual Studio 2010 появилась новая функциональность - обобщенная ковариантность и контравариантность (generic co- and contravariance), - доступная при работе с обобщенными интерфейсами и делегатами. В версиях до Visual Studio 2010 и Microsoft .NET Framework 4 обобщения вели себя инвариантно по отношению к подтипам (subtyping), поэтому преобразования между обобщенными типами с разными аргументами-типами не разрешались. Например, если бы вы попытались передать List(Of Derived) в метод, принимающий IEnumerable(Of Base), то получили бы ошибку. Но Visual Studio 2010 умеет обрабатывать безопасные в многопоточной среде ковариантность и контравариантность, которые поддерживают объявление ковариантных и контравариантных параметров-типов в обобщенных интерфейсах и типах-делегатах. В этой статье мы обсудим, в чем смысл этой функциональности и как с выгодой использовать ее в приложениях. Поскольку кнопка является элементом управления, можно было бы ожидать, что следующий код будет работать в соответствии с базовыми принципами объектно-ориентированного наследования: Однако в 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 вы хотите вызвать. Для этого он использует следующий алгоритм. · Генерируется набор всех применимых кандидатов поиском всего, что имеет имя Foo. В нашем примере таких кандидатов два. · Для каждого кандидата просматривается список аргументов и отсеиваются неприменимые функции. Заметьте, что компилятор также выполняет кое-какую проверку и логически распознает типы для обобщений. С введением вариантности набор предопределенных преобразований расширен, и в результате на этапе 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 Ограничения ко- и контравариантности Вот список таких ограничений. · Контекстуальные ключевые слова In и Out могут появляться в объявлениях интерфейсов или делегатов. Применение этих ключевых слов в любом другом объявлении дает ошибку компиляции. В вариантный интерфейс нельзя вкладывать класс или структуру, но он может содержать вложенные интерфейсы и делегаты, которые в таком случае унаследуют вариантность от содержащего их типа. · Только интерфейсы и делегаты могут быть ковариантными или контравариантными и только в том случае, когда аргументы-типы являются ссылочными типами (reference types). · Вариантное преобразование нельзя выполнять над вариантными интерфейсами, если их экземпляры были созданы с использованием значимых типов (value types). · Перечисления, классы, события и структуры нельзя включать в вариантный интерфейс. Дело в том, что мы создаем эти классы/структуры/перечисления как обобщенные, наследуя обобщенные параметры их контейнеров, поэтому в итоге они унаследуют вариантность своих контейнеров. А вариантность классов/структур/перечислений запрещена спецификацией 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.
|
|