Подтвердить что ты не робот

С# Невозможно использовать параметр ref или out внутри тела анонимного метода

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

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

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

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

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

4b9b3361

Ответ 1

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

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);
    }

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

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

(И да, так, как я работал над этим вопросом прежде, чем задавать этот вопрос, но просто пытался выяснить, есть ли способ избавиться от всех переменных Reference <int> и просто использовать обычные int. О, хорошо.)

Ответ 2

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

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

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

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

Ответ 3

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

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

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