【问题标题】:Prevent .NET from "lifting" local variables防止 .NET “提升”局部变量
【发布时间】:2008-09-21 08:24:04
【问题描述】:

我有以下代码:

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

因为编译器用闭包替换前缀变量“NEW:brownie”被打印到控制台。

是否有一种简单的方法可以防止编译器在仍然使用 lambda 表达式的同时解除前缀变量?我想要一种使我的 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;
    }
}

这似乎需要做很多额外的工作才能让编译器变得“哑巴”。

【问题讨论】:

  • 所以你的问题不是关于惰性评估,而是关于序列化?

标签: c# .net lambda


【解决方案1】:

我现在看到了根本问题。它比我最初想象的要深。基本上解决方案是在序列化之前修改表达式树,通过用常量节点替换所有不依赖于参数的子树。这显然被称为“功能化”。 here有一个解释。

【讨论】:

【解决方案2】:

再做一个闭包...

比如说:

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,因此尚未对其进行测试,但通常情况下,这就是我解决此类问题的方法。

【讨论】:

  • 虽然这确实输出了“OLD:Brownie”,但仍然形成了一个闭包,因此序列化仍然失败。
【解决方案3】:

Lambdas 会自动“吸收”局部变量,恐怕这就是它们的定义方式。

【讨论】:

    【解决方案4】:

    这是一个非常常见的问题,即变量被一个闭包无意中修改 - 一个更简单的解决方案就是:

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

    某些前缀运算符会导致在构造匿名委托/函数之前评估变量的值。

    【讨论】:

    • 我认为您错过了问题的重点。您的委托将无法序列化,因为闭包仍然形成,只是在不同的变量上。
    【解决方案5】:

    我现在遇到了问题:lambda 引用了可能不可序列化的包含类。然后做这样的事情:

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

    (注意 static 关键字。)然后 lambda 不需要引用包含类。

    【讨论】:

    • 几乎成功了!不幸的是,编译器生成了一个私有密封类来保存前缀变量。编译器生成的类不可序列化。
    【解决方案6】:

    这里已经有几个答案解释了如何避免 lambda “提升”你的变量。不幸的是,这并不能解决您的根本问题。无法序列化 lambda 与“提升”变量的 lambda 无关。如果 lambda 表达式需要一个非序列化类的实例来计算,那么它不能被序列化是完全合理的。

    根据您实际尝试做的事情(我不能从您的帖子中完全决定),一种解决方案是将 lambda 的不可序列化部分移到外面。

    例如,而不是:

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

    【讨论】:

    • 如果该代码包含在一个方法不可序列化的类中(在我的例子中是一个 ASP.NET 代码隐藏),那么 prependAction 仍将引用该类而不是序列化。
    【解决方案7】:

    这个呢

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

    【讨论】:

    • 这并不能解决序列化问题,因为 _prefix 变量仍然附加到包含代码的类。
    【解决方案8】:

    怎么样:

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

    ?

    【讨论】:

    • 这并不能解决序列化问题,因为 prefixCopy 变量仍然附加到包含代码的类。
    【解决方案9】:

    好吧,如果我们要在这里讨论“问题”,lambdas 来自函数式编程世界,在纯粹的函数式编程语言中,没有分配,所以你的问题永远不会出现是因为前缀的值永远不会改变。我知道 C# 认为从函数式程序中导入想法很酷(因为 FP 很酷!)但很难让它变得漂亮,因为 C# 是并且将永远是一种命令式编程语言。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-03-18
      • 2015-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-20
      • 2015-02-27
      相关资源
      最近更新 更多