C # Не может использовать параметр ref или out внутри тела анонимного метода - PullRequest
40 голосов
/ 21 ноября 2010

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

public static class IntEx {
    public static Action CreateIncrementer(ref int reference) {
        return () => {
            reference += 1;
        };
    }
}

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

public static class IntEx {
    public static Action CreateIncrementer(Func<int> getter, Action<int> setter) {
        return () => setter(getter() + 1);
    }
}

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

Ответы [ 3 ]

31 голосов
/ 21 ноября 2010

Хорошо, я обнаружил, что на самом деле возможно с указателями, если в небезопасном контексте:

public static class IntEx {
    unsafe public static Action CreateIncrementer(int* reference) {
        return () => {
            *reference += 1;
        };
    }
}

Однако сборщик мусора может нанести ущерб этому, переместив вашу ссылку во время сбора мусора, как показано ниже:

class Program {
    static void Main() {
        new Program().Run();
        Console.ReadLine();
    }

    int _i = 0;
    public unsafe void Run() {
        Action incr;
        fixed (int* p_i = &_i) {
            incr = IntEx.CreateIncrementer(p_i);
        }
        incr();
        Console.WriteLine(_i); // Yay, incremented to 1!
        GC.Collect();
        incr();
        Console.WriteLine(_i); // Uh-oh, still 1!
    }
}

Эту проблему можно обойти, закрепив переменную в определенном месте в памяти. Это можно сделать, добавив в конструктор следующее:

    public Program() {
        GCHandle.Alloc(_i, GCHandleType.Pinned);
    }

Это мешает сборщику мусора перемещать объект, именно то, что мы ищем. Однако затем вы должны добавить деструктор, чтобы освободить булавку, и он фрагментирует память в течение всего срока службы объекта. Не намного проще. Это имело бы больше смысла в C ++, где вещи не перемещаются, а управление ресурсами - само собой разумеется, но не так много в C #, где все это должно быть автоматическим.

Похоже, что мораль этой истории такова, просто оберните этот элемент int в ссылочный тип и покончите с этим.

(И да, я так и работал, прежде чем задавать вопрос, но просто пытался выяснить, можно ли как-нибудь избавиться от всех моих переменных-членов Reference и просто использовать обычные целые числа. Ну хорошо.)

24 голосов
/ 21 ноября 2010

Это невозможно.

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

CLR не позволяет хранить типы ref в полях.

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

2 голосов
/ 21 ноября 2010

Возможно, для среды выполнения полезной была возможность создания ссылок на переменные с механизмом, предотвращающим их сохранение;такая функция позволила бы индексатору вести себя как массив (например, словарьможно получить доступ через "myDictionary [5] .X = 9;").Я думаю, что такую ​​функцию можно было бы обеспечить безопасно, если бы такие ссылки не могли быть переданы другим объектам других типов, ни использовались в качестве полей, ни передавались самими ссылками (поскольку любое место, где могла бы храниться такая ссылка, выходило бы из области видимости до ссылкисам бы).К сожалению, CLR не предоставляет такой возможности.

Для реализации того, что вам нужно, требуется, чтобы вызывающий любой функции, которая использует опорный параметр в замыкании, должен был заключаться взакрытие любой переменной, которую он хочет передать такой функции.Если бы существовало специальное объявление, указывающее, что параметр будет использоваться таким образом, для компилятора может оказаться целесообразным реализовать требуемое поведение.Может быть, в компиляторе .net 5.0, хотя я не уверен, насколько это было бы полезно.

Кстати, я понимаю, что замыкания в Java используют семантику по значению, в то время как те, что в .net, являются ссылочными.,Я могу понять некоторые случайные варианты использования семантики по ссылкам, но использование ссылки по умолчанию кажется сомнительным решением, аналогичным использованию семантики передачи параметров по умолчанию по ссылкам для версий VB вплоть до VB6.Если кто-то хочет захватить значение переменной при создании делегата для вызова функции (например, если кто-то хочет, чтобы делегат вызвал MyFunction (X), используя значение X при создании делегата), лучше использовать лямбдус дополнительной температурой, или лучше просто использовать фабрику делегатов и не беспокоиться о лямбда-выражениях.

...