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 选项?

4

3 回答 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);
    }

这可以防止垃圾收集器移动对象,这正是我们正在寻找的。但是,您必须添加一个析构函数来释放 pin,并且它会在对象的整个生命周期中对内存进行分段。真的没有更容易。这在 C++ 中会更有意义,因为 C++ 中的东西不会被移动,资源管理是正常的,但在 C# 中就不那么重要了,因为所有这些都应该是自动的。

所以看起来故事的寓意是,只需将该成员 int 包装在引用类型中并完成它。

(是的,这就是我在提出问题之前的工作方式,但只是想弄清楚是否有一种方法可以摆脱我的所有 Reference<int> 成员变量并只使用常规整数。哦,好吧。 )

于 2010-11-21T04:14:15.610 回答
25

这是不可能的。

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

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

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

于 2010-11-21T02:30:18.360 回答
2

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

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

顺便说一句,我的理解是 Java 中的闭包使用按值语义,而 .net 中的闭包是按引用。我可以理解引用语义的一些偶尔用途,但默认使用引用似乎是一个可疑的决定,类似于 VB6 之前的 VB 版本使用默认的引用参数传递语义。如果想在创建委托时捕获变量的值来调用函数(例如,如果希望委托在创建委托时使用 X 的值调用 MyFunction(X)),那么使用 lambda 会更好吗?有一个额外的临时,或者最好简单地使用委托工厂而不用打扰 Lambda 表达式。

于 2010-11-21T06:07:55.893 回答