Почему не делегировать работу по контравариантности со значениями?

Этот фрагмент не компилируется в LINQPad.

void Main()
{
    (new[]{0,1,2,3}).Where(IsNull).Dump();
}

static bool IsNull(object arg) { return arg == null; }

Сообщение об ошибке компилятора:

Нет перегрузки для 'UserQuery.IsNull(object)' соответствует делегату 'System.Func'

Он работает для строкового массива, но не работает для int[]. По-видимому, это связано с боксом, но я хочу узнать подробности.

Ответ 1

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

Func<int> f1 = ()=>123;
Func<object> f2 = f1; // Suppose this were legal.
object ob = f2();

Хорошо, что происходит? f2 является ссылочным-идентичным f1. Поэтому, что бы ни делал f1, f2 делает. Что делает f1? Он помещает 32-битное целое в стек. Что делает задание? Он принимает все, что находится в стеке, и сохраняет его в переменной ob.

Где была инструкция по боксу? Не было никого! Мы просто сохранили 32-битное целое число в хранилище, которое ожидало не целое число, а скорее 64-разрядный указатель на кучу, содержащее целое число в штучной упаковке. Таким образом, вы просто смешали стек и исказили содержимое переменной с недопустимой ссылкой. Скоро процесс упадет в пламени.

Итак, куда должна идти инструкция по боксу? Компилятор должен сгенерировать инструкцию по боксу. Он не может идти после вызова f2, потому что компилятор считает, что f2 возвращает объект, который уже был помещен в бокс. Он не может идти в вызове f1, потому что f1 возвращает int, а не вложенное значение int. Он не может проходить между вызовом f2 и вызовом f1, поскольку он является одним и тем же делегатом; между "нет".

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

Func<object> f2 = ()=>(object)f1();

и теперь мы больше не имеем ссылочного идентификатора между f1 и f2, так что в чем разница? Весь смысл наличия ковариантных ссылочных преобразований заключается в сохранении ссылочной идентичности.

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

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

Это иллюстрирует интересное различие в философии дизайна С# и VB. В С# команда разработчиков всегда думает: "Как может компилятор найти то, что может быть ошибкой в ​​пользовательской программе и привлечь его к себе?" и команда VB думает: "Как мы можем понять, что, вероятно, должен произойти, и просто сделать это от их имени?" Короче говоря, философия С# - "если вы что-то видите, говорите что-то", а философия VB "сделайте, что я имею в виду, а не то, что я говорю". Оба являются вполне разумными философиями; Интересно видеть, как два языка, которые имеют почти идентичные наборы функций, отличаются этими маленькими деталями из-за принципов дизайна.

Ответ 2

Потому что Int32 - тип значения, а contra-variance не работает с типами значений.

Вы можете попробовать следующее:

(new **object**[]{0,1,2,3}).Where(IsNull).Dump();

Ответ 3

Это не работает для int, потому что нет объектов.

Попробуйте:

void Fun()
{
    IEnumerable<object> objects = (new object[] { 0, 1, null, 3 }).Where(IsNull);

    foreach (object item in objects)
    {
        Console.WriteLine("item is null");
    }
}

bool IsNull(object arg) { return arg == null; }