Запретить .NET «поднимать» локальные переменные

StackOverflow https://stackoverflow.com/questions/110536

  •  02-07-2019
  •  | 
  •  

Вопрос

У меня есть следующий код:

string prefix = "OLD:";
Func<string, string> prependAction = (x => prefix + x);
prefix = "NEW:";
Console.WriteLine(prependAction("brownie"));

Поскольку компилятор заменяет префиксную переменную замыканием, на консоль выводится «NEW:brownie».

Есть ли простой способ запретить компилятору поднимать префиксную переменную, продолжая использовать лямбда-выражение?Мне нужен способ заставить мой Func работать идентично:

Func<string, string> prependAction = (x => "OLD:" + x);

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

Единственный способ обойти это, который я вижу на данный момент, — это создать новый сериализуемый класс, который хранит строку как переменную-член и имеет метод добавления строки:

string prefix = "NEW:";
var prepender = new Prepender {Prefix = prefix};
Func<string, string> prependAction = prepender.Prepend;
prefix = "OLD:";
Console.WriteLine(prependAction("brownie"));

С вспомогательным классом:

[Serializable]
public class Prepender
{
    public string Prefix { get; set; }
    public string Prepend(string str)
    {
        return Prefix + str;
    }
}

Кажется, что требуется много дополнительной работы, чтобы сделать компилятор «тупым».

Это было полезно?

Решение

Теперь я вижу основную проблему.Это глубже, чем я думал сначала.По сути, решение состоит в том, чтобы изменить дерево выражений перед его сериализацией, заменив все поддеревья, которые не зависят от параметров, постоянными узлами.Это, по-видимому, называется «функлетизацией».этому есть объяснение здесь.

Другие советы

Просто сделайте еще одно замыкание...

Скажем, что-то вроде:

var prepend = "OLD:";

Func<string, Func<string, string>> makePrepender = x => y => (x + y);
Func<string, string> oldPrepend = makePrepender(prepend);

prepend = "NEW:";

Console.WriteLine(oldPrepend("Brownie"));

Я еще не проверял это, так как на данный момент у меня нет доступа к VS, но обычно я решаю такую ​​проблему именно так.

Лямбды автоматически «засасывают» локальные переменные, боюсь, именно так они и работают по определению.

Это довольно распространенная проблема, т.е.переменные непреднамеренно изменяются замыканием - гораздо более простое решение - просто пойти:

string prefix = "OLD:";
var actionPrefix = prefix;
Func<string, string> prependAction = (x => actionPrefix + x);
prefix = "NEW:";
Console.WriteLine(prependAction("brownie"));

Если вы используете resharper, он фактически определит места в вашем коде, где вы рискуете вызвать неожиданные побочные эффекты, такие как этот, поэтому, если файл «все зеленый», ваш код должен быть в порядке.

Я думаю, что в некотором смысле было бы неплохо, если бы у нас был некоторый синтаксический сахар, чтобы справиться с этой ситуацией, чтобы мы могли написать ее в виде одной строки, т.е.

Func<string, string> prependAction = (x => ~prefix + x);

Если какой-либо префиксный оператор приведет к оценке значения переменной до создания анонимного делегата/функции.

Теперь я понимаю проблему:лямбда относится к содержащему классу, который может быть не сериализуемым.Затем сделайте что-то вроде этого:

public void static Func<string, string> MakePrependAction(String prefix){
    return (x => prefix + x);
}

(Обратите внимание на ключевое слово static.) Тогда лямбда-выражению не обязательно ссылаться на содержащий его класс.

Здесь уже есть несколько ответов, объясняющих, как избежать того, чтобы лямбда «поднимала» вашу переменную.К сожалению, это не решает вашу основную проблему.Невозможность сериализовать лямбду не имеет ничего общего с тем, что лямбда «подняла» вашу переменную.Если для вычисления лямбда-выражения требуется экземпляр несериализуемого класса, вполне логично, что его нельзя сериализовать.

В зависимости от того, что вы на самом деле пытаетесь сделать (я не могу определиться с вашим сообщением), решением было бы переместить несериализуемую часть лямбды наружу.

Например, вместо:

NonSerializable nonSerializable = new NonSerializable();
Func<string, string> prependAction = (x => nonSerializable.ToString() + x);

использовать:

NonSerializable nonSerializable = new NonSerializable();
string prefix = nonSerializable.ToString();
Func<string, string> prependAction = (x => prefix + x);

Как насчет этого

string prefix = "OLD:";
string _prefix=prefix;
Func<string, string> prependAction = (x => _prefix + x);
prefix = "NEW:";
Console.WriteLine(prependAction("brownie"));

Как насчет:

string prefix = "OLD:";
string prefixCopy = prefix;
Func<string, string> prependAction = (x => prefixCopy + x);
prefix = "NEW:";
Console.WriteLine(prependAction("brownie"));

?

Что ж, если мы собираемся говорить здесь о «проблемах», то лямбды пришли из мира функционального программирования, а в чисто функциональном языке программирования: нет никаких заданий и поэтому ваша проблема никогда не возникнет, потому что значение префикса никогда не изменится.Я понимаю, что C# считает, что импортировать идеи из функциональных программ — это круто (потому что FP является круто!), но сделать это красиво очень сложно, потому что C# есть и всегда будет императивным языком программирования.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top