在C#中存储对象的引用

5

我想知道如何在 .net 中存储对象的引用。

也就是说,我想要像以下代码一样实现(当然,下面的示例代码可能与实际操作方式有很大偏差):

class Test
{
    private /*reference to*/ Object a;
    public Test(ref int a)
    {
        this.a = a;
        this.a = ((int)this.a) + 1;
    }
    public Object getA() { return this.a; }
}
/*
 * ...
 */
static void Main(string[] args)
{
    int a;
    a=3;
    Test t = new Test(ref a);
    Console.WriteLine(a);
    Console.WriteLine(t.getA());
    Console.ReadKey();
}

产生以下输出:
4
4

理想情况下,我希望在不编写整数的包装类的情况下完成此操作。换句话说,我认为我想要在 .Net 中使用指针。

4
我建议您阅读有关CLR中“值类型”和“引用类型”之间区别的内容。 - Kennet Belenky
2
在.NET中有指针,但你敢用它们来做这个吗?哈哈 - Rune FS
@JohnSaunders 什么是 [c#-language] 和 [c#] 的区别?它们不是同义词吗?我不明白。 - Pascal Thivent
@Pascal:我添加了该标签,以指示像这样实际上是关于C#编程语言的问题,而不是大多数标记为C#的问题。那些其他问题大多是关于其他事情的,但提问者碰巧正在使用C#。 - John Saunders
请注意,对于我添加的新标签似乎存在一些分歧:http://meta.stackexchange.com/questions/62669/congratulations-eric-lippert-for-finally-winning-the-c-language-badge。 - John Saunders
2个回答

36
你不能在.NET中存储变量的引用。你可以存储对象的引用,但不能存储变量的引用。
原因是,如果允许存储任意变量的引用,那么就可以存储对局部变量的引用。如果可以存储对局部变量的引用,则运行时无法使用将局部变量存储在短期内存池(即堆栈)上的优化。
即使可以这样做,你所描述的操作也不是类型安全的另一个原因。你有一个名为“a”的字段变量,类型为“引用到对象变量”,以及一个名为“a”的本地变量,类型为“引用到int变量”。即使可以存储变量的引用,也没有任何意义将int变量的引用存储在类型为“引用到对象变量”的内容中,因为这两种类型在逻辑上不兼容。你可以对它们执行的操作是不同的;可以将字符串写入对象变量的引用中,但是不能将其写入int变量的引用中。
也许我误解了,但是像上面的整数变量这样的变量不会被装箱成对象,然后可以将其作为引用存储吗?
你混淆了对对象的引用和对变量的引用。我们使用相同的术语来表示实际上是两个不同的事物,这很令人困惑。
是的,装箱将值类型(如int)转换为引用类型(如object)。但这与对变量的引用没有任何关系。
当你创建对变量的引用时,你正在为该变量创建一个别名。当你说
void M(ref int y) { y = 123; }
...
int x = 0;
M(ref x);

您在说“x和y是同一个变量的两个不同名称”。

如果您想表示“我已经捕获了一个变量,并且想要能够读写它”的概念,那么请使用委托:

class Ref<T>
{
    private Func<T> getter;
    private Action<T> setter;
    public Ref(Func<T> getter, Action<T> setter)
    {
        this.getter = getter;
        this.setter = setter;
    }
    public T Value
    {
        get { return getter(); }
        set { setter(value); }
    }
}
...
int abc = 123;
var refabc = new Ref<int>(()=>abc, x=>{abc=x;});
... now you can pass around refabc, store it in a field, and so on
refabc.Value = 456;
Console.WriteLine(abc); // 456
Console.WriteLine(refabc.Value); // 456

明白了吗?

1
@Maciej:为什么会涉及装箱?装箱是将转换为对象的过程。你为什么认为这与构建闭包有关呢?而且你认为栈与此有什么关系?封闭的局部变量首先就没有存储在栈上。 - Eric Lippert
1
但实际上,你不需要担心这些。编译器和运行时会根据数据的使用方式自动确定最有效存储数据的位置。幕后有各种疯狂的黑魔法一直在发生;不要担心这些魔法,而是关注你的程序语义。 - Eric Lippert
1
@Marcelo:但是你正在问一个实现细节的问题,因此不可能回答这个问题而不涉及实现细节。从这个角度来看:变量被定义为存储位置。因此,“对变量的引用”必须是对存储位置的引用。当您将一个变量传递给一个带有“ref”参数的方法时,实际传递的是对变量的引用——存储位置的托管地址。这是一个实现细节。 - Eric Lippert
1
@Marcelo:当你提升一个lambda的外部变量时,我们会生成代码告诉运行时“确保与此本地变量相关联的存储在堆上而不是栈上”。在这里我们没有生成任何获取托管地址的本地变量的代码。这里有两个概念:(1)生成代码以获取存储位置的托管地址以引用变量,(2)生成代码以确保特定存储在长期使用的堆上而不是短期使用的池中。这些是完全不同的两件事,彼此之间没有任何关系。 - Eric Lippert
垃圾回收怎么办?如果垃圾收集器决定将我的对象移动到不同的内存集群,这个程序就会失败吗?我认为你想要存储引用的内容必须被固定在内存中,以便防止这种巨大的中断。 - rbaleksandar
显示剩余17条评论

10

C#中没有类似于C++的int& a的引用变量概念。但有一些解决方法,其中之一是使用闭包:

class Test
{
    private Func<int> get_a;
    private Action<int> set_a;
    public Test(Func<int> get_a, Action<int> set_a)
    {
        this.get_a = get_a;
        this.set_a = set_a;
        this.set_a(this.get_a() + 1);
    }
    public Object getA() { return this.get_a(); }
}
/*
 * ...
 */
static void Main(string[] args)
{
    int a;
    a=3;
    Test t = new Test(() => a, n => { a = n; });
    Console.WriteLine(a);
    Console.WriteLine(t.getA());
    Console.ReadKey();
}

我不在VS前面,所以请原谅任何尴尬的失误。


2
+1,虽然很可爱,但我不会在生产代码中使用它。 - Pierreten

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