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

时间:2010-11-21 02:25:14

标签: c# lambda anonymous-methods ref

我正在尝试创建一个可以创建一个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选项?

3 个答案:

答案 0 :(得分:31)

好吧,如果在不安全的环境中,我发现它实际上 可以使用指针:

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&lt; int&gt;成员变量而只是使用常规的int。哦,好吧。)

答案 1 :(得分:24)

这是不可能的。

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

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

例如,如果将局部变量中的值类型作为ref参数传递,则值的生命周期将超出其堆栈帧。

答案 2 :(得分:2)

对于运行时来说,允许创建具有防止其持久性的机制的变量引用可能是一个有用的功能;这样的特性将允许索引器表现得像一个数组(例如,因此可以通过“myDictionary [5] .X = 9;”来访问字典&lt; Int32,Point&gt;)。我认为这样的特征可以安全地提供,如果这样的引用不能被转发到其他类型的对象,也不能用作字段,也不能通过引用本身传递(因为任何可以存储这样的引用的地方都会超出引用范围之前)本身就会)。不幸的是,CLR没有提供这样的功能。

要实现你所需要的,将要求在闭包内使用引用参数的任何函数的调用者必须在闭包中包含它想要传递给这样一个函数的任何变量。如果有一个特殊声明表明将以这种方式使用参数,那么编译器实现所需行为可能是切实可行的。也许在.net 5.0编译器中,虽然我不确定它会有多大用处。

BTW,我的理解是Java中的闭包使用按值语义,而.net中的闭包是引用。我可以理解一些偶然使用的引用语义,但默认情况下使用引用似乎是一个可疑的决定,类似于通过VB6使用VB版本的默认引用参数传递语义。如果想要在创建调用函数的委托时捕获变量的值(例如,如果想要委托在创建委托时使用X的值调用MyFunction(X)),那么使用lambda是否更好?有额外的临时性,或者只是简单地使用委托工厂而不打扰Lambda表达式。