Действие делегатов, дженерики, ковариация и контравариантность - PullRequest
10 голосов
/ 02 августа 2011

У меня есть два класса бизнес-контрактов:

public BusinessContract

public Person : BusinessContract

В другом классе у меня есть следующий код:

private Action<BusinessContract> _foo;

public void Foo<T>( Action<T> bar ) where T : BusinessContract
{
    _foo = bar;
}

Выше не будет даже компилировать, что немного сбивает меня с толку. Я ограничиваю T быть BusinessContract, так почему компилятор не знает, что bar может быть назначена _foo?

Пытаясь обойти это, мы попытались изменить его на следующее:

public void Foo<T>( Action<T> bar ) where T : BusinessContract
{
    _foo = (Action<BusinessContract>)bar;
}

Теперь компилятор доволен, поэтому я пишу следующий код в другом месте моего приложения:

Foo<Person>( p => p.Name = "Joe" );

И приложение взрывается с InvalidCastException во время выполнения.

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

UPDATE

Джон ответил на вопрос, поэтому получил кивок за это, но просто чтобы замкнуть цикл на этом, вот как мы закончили с решением проблемы.

private Action<BusinessContract> _foo;

public void Foo<T>( Action<T> bar ) where T : BusinessContract
{
    _foo = contract => bar( (T)contract );
}

Почему мы это делаем? У нас есть поддельный DAL, который мы используем для модульного тестирования. С помощью одного из методов нам нужно дать разработчику теста возможность указать, что должен делать метод при вызове во время теста (это метод обновления, который обновляет кэшированный объект из базы данных). Цель Foo - установить, что должно происходить, когда вызывается обновление. IOW, в другом месте этого класса у нас есть следующее.

public void Refresh( BusinessContract contract )
{
    if( _foo != null )
    {
        _foo( contract );
    }
}

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

Foo<Person>( p => p.Name = "New Name" );

Ответы [ 2 ]

17 голосов
/ 02 августа 2011

У вас есть ковариация и контравариантность в неправильном направлении. Давайте рассмотрим Action<object> и Action<string>. Удаляя фактические генерики, вы пытаетесь сделать что-то вроде этого:

private Action<object> _foo;

public void Foo(Action<string> bar)
{
    // This won't compile...
    _foo = bar;
}

Теперь предположим, что мы тогда напишем:

_foo(new Button());

Это нормально, потому что Action<object> может быть передан любому объекту ... но мы инициализировали его делегатом, который должен принять строковый аргумент. Уч.

Это небезопасно, поэтому не компилируется.

Другой способ будет работать, хотя:

private Action<string> _foo;

public void Foo(Action<object> bar)
{
    // This is fine...
    _foo = bar;
}

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

Таким образом, в основном Action<T> является контравариантным - тогда как Func<T> является ковариантным :

Func<string> bar = ...;
Func<object> foo = bar; // This is fine
object x = foo(); // This is guaranteed to be okay

Не ясно, что вы пытаетесь делать с действием, поэтому, к сожалению, я не могу дать какой-либо совет, как это обойти ...

0 голосов
/ 02 августа 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...