C#无法在匿名方法体内使用ref或out参数

41

我正在尝试创建一个函数,该函数可以创建一个Action来增加传入的任何整数值。但是我的第一次尝试会出现错误"无法在匿名方法体内使用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);
    }
}

但是对于调用者来说,这样做会更加困难;需要调用者创建两个lambda表达式而不是只传递一个引用。是否有更优雅的方式提供这个功能,还是我只能接受使用两个lambda表达式的选项?


2
这是一个简化的例子吗?为什么不使用x++?为什么另一个类要增加这个类的状态? - Gishu
2
@Gishu 是的,这只是一个简化的示例;更大的用例难以解释,但归根结底都是要创建一个可以在值类型上执行操作的Action工厂。 - Dax Fohl
3个回答

32

好的,我发现在不安全的上下文中,使用指针是确实可能的:

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<int> 成员变量,只使用普通的 int 而已。)


4
有趣的解决托管模式的方法,使用不安全的上下文进行+1。 - John K
不需要使用GCHandle.Alloc - 只需扩展fixed语句的花括号即可。 - Mr. TA
@Mr.TA 这只是一个示例,以展示GC可能会搞砸事情。任何有价值的incr函数的使用可能会从创建它的方法中返回它,或将其添加到持久对象作为成员变量,这显然会留下“固定”的范围。 - Dax Fohl
2
@Dax Fohl没错 - 我觉得在我们两人的评论之后,现在更清楚了。 - Mr. TA

25

这是不可能的。

编译器会将匿名方法使用的所有局部变量和参数转换为自动生成的闭包类中的字段。

CLR不允许将ref类型存储在字段中。

例如,如果您将一个值类型的本地变量作为ref参数传递,该值的生命周期将延伸超出其堆栈帧。


2
可能为运行时创建变量引用并防止其持久化是一个有用的功能;这样的功能将允许索引器像数组一样运作(例如,可以通过“myDictionary[5].X = 9”来访问Dictionary<Int32, Point>)。如果这样的引用不能被向下转换为其他类型的对象,也不能用作字段或传递给其他引用(因为任何可以存储此类引用的位置都会在引用本身之前超出范围),那么这样的功能应该是安全的。不幸的是,CLR没有提供这样的功能。
要实现你想要的功能,需要调用任何在闭包中使用引用参数的函数的“调用者”必须在闭包中包装它想要传递给这样的函数的任何变量。如果有一个特殊的声明来指示参数将以这种方式使用,编译器可能会实现所需的行为。也许在.NET 5.0编译器中,尽管我不确定这有多有用。
顺便说一句,我理解Java中的闭包使用按值语义,而.NET中的闭包使用按引用语义。我可以理解一些偶尔使用按引用语义的情况,但默认使用引用似乎是一个可疑的决定,类似于VB版本直到VB6的默认按引用传递参数的语义。如果想要在创建调用函数的委托时捕获变量的值(例如,如果想要一个委托使用创建委托时X的值来调用MyFunction(X)),是使用带有额外临时变量的Lambda更好,还是直接使用委托工厂并不烦恼Lambda表达式?

1
是的,Eric Lippert写了一篇关于这个问题的博客。http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx。实际上,我有依赖于这种行为的代码;例如在OpenGL循环中保持帧计数而不需要成员变量。我更喜欢C++规范,因为它允许任何语义。 - Dax Fohl
1
@Dax:当然有些情况下需要传递引用语义。同样,参数传递也是如此。这并不意味着传递引用应该成为默认选项。顺便说一句,我想知道使用单元素数组替换按引用闭包变量的优缺点是什么,从而允许为需要不同组合的闭包变量的匿名方法创建不同的类(避免一些GC问题)。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接