C#属性和ref参数,为什么没有简化语法?

72

我在使用C#时遇到了这个错误信息:

属性或索引器不能作为 out 或 ref 参数传递

我知道这个错误的原因,并且采取了快速解决方案,创建一个正确类型的本地变量,将其作为 out/ref 参数调用函数,然后将其赋回属性:

RefFn(ref obj.prop);

转化为
{
    var t = obj.prop;
    RefFn(ref t);
    obj.prop = t;
}

如果属性在当前上下文中不支持 get 和 set,则此方法显然会失败。

为什么C#不直接为我做到这一点呢?


我能想到的唯一可能产生问题的情况是:

  • 线程
  • 异常

对于线程,这种转换会影响写入发生的时间(在函数调用之后与在函数调用中),但我认为任何依赖于此的代码在出现问题时都将得到很少的同情。

对于异常,问题在于:如果函数分配给多个 ref 参数之一,那么会发生什么情况?任何简单的解决方案都会导致所有参数都被分配或者全部不被分配。再次强调,我认为这不是语言的所支持的使用方式。


注意:我理解为什么会生成这个错误信息的机制。我想知道的是,为什么C#不自动实现这个微不足道的解决方法的原因。


5
通往编写甜蜜编译器的路上充满着善意。 - BC.
它不执行变通方法,因为这并不总是安全的。更根本的问题是,类型为T的属性不应该只有getter和setter方法,而应该还有一个ActUpon<U>([indexparams...,] ActionByRef<T,U> act, ref U param)方法。这样的方法将允许像ListOfPoints[4].X += something这样的语句被高效地执行,使用静态委托和无闭包(ListOfPoints.ActUpon(4, (ref Point it, ref param) => {it.X += param;}, ref something);)。 - supercat
在这个答案的底部,使用表达式来解决这个问题是一个可靠的方法:https://dev59.com/d07Sa4cB1Zd3GeqP332x#3059448 - Chris Moschini
9个回答

31
因为您传递的是索引器的结果,实际上是方法调用的结果。没有保证索引器属性也有setter,通过ref传递会使开发人员产生错误的安全感,认为属性将在不调用setter的情况下被设置。
更技术层面上,ref和out传递的是被传入对象的内存地址,要设置属性,必须调用setter,因此不能保证属性实际上会被改变,尤其是当属性类型是不可变时。 ref和out不仅仅在方法返回时设置值,它们还会传递对象本身的实际内存引用。

1
+1,需要注意的是P/Invoke可能会出现问题,因为属性所属的对象可能不再具有GC根并被回收!糟糕... - user7116
@sixletter:不是这样的,要让对象被收集,调用代码中对它的引用必须被删除。 - BCS
3
您描述的问题将会在编译时自动解决,因为编译器会尝试解析setter方法,但由于该属性只有get方法,所以解析失败并出现与尝试给只读属性赋值时相同的错误。 - BCS
1
是的,但请记住,C#属性本身就是语法糖。人们已经因为属性看起来像字段而感到困惑。再添加更多的语法糖肯定不会帮助解决这个问题。有一个限度,也应该有一个限度。 - David Morton
如果属性是字符串,则该属性的结果是指向该字符串的指针。ref 是指向该指针的指针。因此,它不应该关心它是否是属性,它应该修复引用。在 VB.net 中完成并且正常工作,但在 C# 中似乎缺少。你是对的,当作为 ref 传递时,setter 被调用,正如应该的那样,并且不能保证属性实际上会被更改。保证的是您的 setter 代码将运行。这也可以在 VB.NET 中验证。 - Brain2000

16

属性不过是Java风格的getX/setX方法的语法糖。在方法上使用'ref'没有多大意义。在您的情况下,这样做是有意义的,因为您的属性只是存根字段。属性不必仅是存根,因此框架不能允许在属性上使用“ref”。

编辑:简单地说,属性getter或setter可能包含的远不止一个字段读写,这使得允许你所提出的语法糖变得不太理想,甚至可能出乎意料。这并不是说我以前没有需要这种功能,而是我理解为什么他们不想提供它。


+1 这是唯一一个不需要读5遍才能理解的解释。 - goku_da_master

12

只是提一下,C# 4.0 将会有类似这样的语法糖,但只在调用交互方法时使用 - 部分原因是在这种情况下ref非常普遍。 我没有在 CTP 中进行过多测试; 我们必须看看它的实际效果 ...


1
专门针对COM方法,C#编译器允许您将参数按值传递到这种方法中,并自动生成临时变量来保存传入的值,调用返回后随即丢弃这些临时变量。 - Marc Gravell
C#不是一种低级语言,因此ref参数不应该是一个地址,而实际上应该是一对闭包,即getter和setter。如果将局部变量作为ref参数传递,语言应自动创建getter/setter对。所有这些都应该对毫无戒心的程序员隐藏起来。 - isekaijin
@Eduardo,这会改变语义并破坏价值类型的方式;这永远不会发生。我也不希望它发生。 - Marc Gravell
@Marc:那会如何改变任何语义呢?毕竟,变量只是一种读取(获取)和写入(设置)一堆内存的机制。 - isekaijin
@Eduardo 不是这样的;对于值类型,它就是数据。访问器(属性)会进行复制。这在可变结构体的情况下是个问题。现在,你可以说我们不应该有可变结构体……但是:我们确实有。 - Marc Gravell

8

你可以使用带有ref/out的字段,但不能使用属性。原因是属性实际上只是特殊方法的语法简写。编译器实际上将get/set属性转换为相应的get_Xset_X方法,因为CLR不直接支持属性。


1
我知道了,看看我的新笔记。 - BCS

6

这不是线程安全的;如果两个线程同时创建属性值的副本并将它们作为 ref 参数传递给函数,那么只有一个会返回到属性中。

class Program
{
  static int PropertyX { get; set; }

  static void Main()
  {
    PropertyX = 0;

    // Sugared from: 
    // WaitCallback w = (o) => WaitAndIncrement(500, ref PropertyX);
    WaitCallback w = (o) => {
      int x1 = PropertyX;
      WaitAndIncrement(500, ref x1);
      PropertyX = x1;
    };
    // end sugar

    ThreadPool.QueueUserWorkItem(w);

    // Sugared from: 
    // WaitAndIncrement(1000, ref PropertyX);
    int x2 = PropertyX;      
    WaitAndIncrement(1000, ref x2);
    PropertyX = x2;
    // end sugar

    Console.WriteLine(PropertyX);
  }

  static void WaitAndIncrement(int wait, ref int i)
  {
    Thread.Sleep(wait);
    i++;
  }
}

PropertyX最终的值为1,而字段或局部变量的值为2。

当要求编译器执行复杂操作时,匿名方法等因素会引入困难,上述代码示例也凸显了这一点。


这是一个很好的观点,但我认为您所展示的是转换可以改变已经存在竞态条件(例如非线程安全代码)的代码的结果。 - BCS
那是真的,在我举的例子中。哎呀。 - Mark Rendle
我不同意你的看法。上面的例子只是展示了x1和x2都被设置为0,然后在500和1000毫秒之后都被增加到1,然后它们都将PropertyX设置为1。没有一个本地变量是2。你需要将PropertyX传递到WaitAndIncrement( )中才能真正测试这个,这只能在VB.NET中完成。然而,它可能仍然无效,因为getter首先被调用。这与它是否线程安全无关。它与使用没有任何交换函数的局部变量一样线程安全。 - Brain2000
请再仔细阅读我的回答,你会发现你其实并不反对我。 - Mark Rendle

5
这是因为C#不支持接受通过引用传递的参数的“有参数属性”。值得注意的是,CLR支持此功能,但C#不支持。

4
VB.net支持它,我很困惑为什么C#不支持。 - Brain2000
有人能回答这个问题吗,如果上面是真的的话? - Paul C

4

当你在参数前加上 ref/out 时,表示你正在传递一个存储在堆中的引用类型。

属性是包装方法,而不是变量。


0
如果你想知道为什么编译器不会替换属性 getter 返回的字段,那是因为 getter 可以返回 const、readonly、literal 或其他不应该被重新初始化或覆盖的内容。

0

这个网站似乎为您提供了一种解决方法。虽然我没有测试过,所以不能保证它会起作用。该示例似乎使用反射来访问属性的get和set函数。这可能不是推荐的方法,但它可能可以实现您所要求的功能。

http://www.codeproject.com/KB/cs/Passing_Properties_byref.aspx


啊,使用反射有时候确实能为你提供解决方案,这并没有什么问题。 - Camilo Martin

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