存储值类型的引用?

47

我正在编写一个"Monitor"对象来方便调试我的应用程序。从IronPython解释器中可以在运行时访问此Monitor对象。我的问题是,在C#中是否可以存储值类型的引用?比如我有以下类:

class Test
{
    public int a;
}

我可以以某种方式存储对“a”的“指针”,以便随时检查它的值吗?是否可以使用安全和托管代码实现?

谢谢。

6个回答

62
你不能将变量的引用存储在字段或数组中。CLR要求变量的引用位于(1)形式参数,(2)局部变量或(3)方法的返回类型中。C#支持(1),但不支持另外两个。
(顺带一提:C#可以支持另外两个;事实上,我编写了一个原型编译器来实现这些功能。它非常棒。 (详见http://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/)。当然,必须编写一个算法来验证没有ref local可能引用现在已被销毁的堆栈帧上的局部变量,这有点棘手,但是可行的。也许我们会在假设未来版本的语言中支持这一点。(更新:它已经添加到C# 7中!)
但是,您可以通过将其放入字段或数组中使变量具有任意长的生命周期。如果您需要的是“引用”的意义上的“我需要存储对任意变量的别名”,那么不行。但是,如果您需要的是“引用”的意义上的“我需要一个神奇的令牌,让我读取和写入特定的变量”,那么只需使用委托或一对委托即可。
sealed 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); }
    }
}
...
Ref<string> M() 
{
    string x = "hello";
    Ref<string> rx = new Ref<string>(()=>x, v=>{x=v;});
    rx.Value = "goodbye";
    Console.WriteLine(x); // goodbye
    return rx;
}

外部局部变量x将至少保持活动状态,直到rx被回收。


为了使其正常工作,语言/框架应允许声明存储位置为持久、可返回或短暂,并且只允许将一个存储类的内容存储到同样或更严格的类中。函数的返回值应该是其声明的返回类或传递给其“可返回”参数中最严格的存储类中更严格的一个。这样的规则不仅对ref参数有用,对其他参数也有用(因此代码可以将可变类对象的引用传递给方法,并知道... - supercat
在很多情况下,这意味着任何可能导致变异的操作都会在相关方法返回之前完成,从而消除了防御性复制的必要性。 - supercat
+1你的答案,因为它解决了持久化问题。然而,让我有点遗憾的是,C#没有机制将值类型存储为引用。我确实理解这可能会对GC造成复杂的影响,但是引用值类型可能真的会对编码技巧产生巨大的影响。 - David Peterson
@DJPeterson:你可以将值类型存储为引用类型;在C#中,这被称为装箱。我想你的意思是相反的:C#没有机制将变量的引用存储为字段的值 - Eric Lippert
1
@Eric Lippert 没错(如果我理解正确的话)。装箱是我研究过的一些东西,但发现缺乏类型安全和空检查是无法忍受的。我可以通过包装器在某种程度上解决这个问题。然而,不能存在任何结构来保存值类型的引用并保留类型信息,这是致命的。没有它,我无法安全地返回引用而不进行装箱,这当然会将类型安全的负担放在消费者而不是提供者身上,完全违背了初衷。 - David Peterson

9
不行 - 在C#中,你不能直接存储值类型的“指针”。
通常情况下,你需要持有 Test 实例的引用,以便可以通过 testInstance.a 访问 a。

但我想可能没有办法动态存储一些东西?比如将它包装在委托中或类似的东西? - Alex Turpin
你可以随时复制它。你可以创建一个闭包,但是它(间接地)存储了对包含类的引用... - Reed Copsey
2
我在网上寻找了很久如何做到这一点。您能否编辑您的答案并展示一个小例子呢?目前我正在使用反射,但它会给出像Monitor.Watch(new Property(Game, "FPS"))这样的东西,我更愿意只是做Monitor.Watch(Game.FPS)或者至少更简单的东西。 - Alex Turpin
你能展示一些代码来演示你(当前)的代码如何工作吗?我建议你将其发布为一个新问题,并寻求改进建议(例如:删除魔术字符串等)。 - Reed Copsey
事实上,在基础的CLR中不可能有“ref”字段。对于允许引用基于堆栈的变量的系统来说,执行验证将更加困难。C#对此无能为力。 - Mehrdad Afshari
我曾以为可以使用装箱来解决问题,但是我发现你实际上不能直接修改或监视字段。你需要将引用类型装箱为 int,而不是字段本身 =P - Merlyn Morgan-Graham

4
您可以创建ref-return委托。这与Erik的解决方案类似,只是它使用返回ref的单个委托,而非getter和setter。
您无法将其用于属性或局部变量,但它返回真实引用(而非仅为拷贝)。
public delegate ref T Ref<T>();

class Test
{
    public int a;
}

static Ref<int> M()
{
    Test t = new Test();
    t.a = 10;

    Ref<int> rx = () => ref t.a;
    rx() = 5;
    Console.WriteLine(t.a); // 5
    return rx;
}

3
这是我想出来的一种模式,我经常使用它。通常是在将属性作为参数传递给父类型的任何对象以供使用的情况下使用,但对于单个实例同样适用。(但不适用于本地范围值类型)
public interface IValuePointer<T>
{
    T Value { get; set; }
}
public class ValuePointer<TParent, TType> : IValuePointer<TType>
{
    private readonly TParent _instance;
    private readonly Func<TParent, TType> _propertyExpression;
    private readonly PropertyInfo _propInfo;
    private readonly FieldInfo _fieldInfo;

    public ValuePointer(TParent instance, 
                        Expression<Func<TParent, TType>> propertyExpression)
    {
        _instance = instance;
        _propertyExpression = propertyExpression.Compile();
        _propInfo = ((MemberExpression)(propertyExpression).Body).Member as PropertyInfo;
        _fieldInfo = ((MemberExpression)(propertyExpression).Body).Member as FieldInfo;
    }

    public TType Value
    {
        get { return _propertyExpression.Invoke(_instance); }
        set
        {
            if (_fieldInfo != null)
            {
                _fieldInfo.SetValue(_instance, value);
                return;
            }
            _propInfo.SetValue(_instance, value, null);
        }
    }
}

这样就可以像这样使用。
class Test
{
    public int a;
}
void Main()
{
    Test testInstance = new Test();
    var pointer = new ValuePointer(testInstance,x=> x.a);
    testInstance.a = 5;
    int copyOfValue = pointer.Value;
    pointer.Value = 6;
}

请注意接口具有更受限制的模板参数集,这使您可以传递指向没有父类型知识的内容的指针。
您甚至可以实现另一个没有模板参数的接口,在任何值类型上调用.ToString(不要忘记首先进行空值检查)。

1
您可以使用unsafe代码直接获取值类型的指针。
public class Foo
{
    public int a;
}

unsafe static class Program
{
    static void Main(string[] args)
    {
        var f=new Foo() { a=1 };
        // f.a = 1

        fixed(int* ptr=&f.a)
        {
            *ptr=2;
        }
        // f.a = 2
    }
}

这仅适用于内在数值类型。CLR不允许Foo*类型。 - John Alexiou

-4
class Test
{
    private int a;

    /// <summary>
    ///  points to my variable type interger,
    ///  where the identifier is named 'a'.
    /// </summary>
    public int A
    {
        get { return a; }
        set { a = value; }
    }
}

为什么要经历编写复杂代码、在各处声明标识符并链接到同一位置的所有麻烦呢?创建一个属性,在类外添加一些 XML 代码,并在编码中使用这些属性。
我不知道存储指针怎么办,不认为这是可能的,但如果您只想检查其值,我所知道的最安全的方法是创建变量的属性。至少这样,您可以随时检查其属性,如果变量是静态的,甚至不必创建类的实例来访问变量。
属性有很多优点,类型安全是其中之一,XML 标记是另一个。开始使用它们吧!

2
这似乎并没有回答问题。问题的提出包括一个公共变量,这几乎与您建议的公共属性相同。 - Michael Myers
表情符号是聊天客户端的习惯。我不确定你所说的粗话是什么意思,我只是在表达我的想法。据我所知,他想要一个指向变量的标识符,如果可能的话,他必须(据我所知)使变量或访问它的某些东西公开。我在说,好吧,为什么不使用属性呢?在 MAIN 或其他地方创建他的类“monitor”的实例时,使用该属性来检查其值。或者将其设置为静态,并像这样访问该属性:namespace.class.property,当他需要检查该值时。这是一种替代方法。 - Gorlykio

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