能否将属性作为“out”或“ref”参数传递?

35

如果可以,我能否将属性作为"out"或"ref"参数传递?如果不行,为什么呢?

例如:

Person p = new Person(); 

. . .

public void Test(out p.Name);

1
请重新输入您的问题,以使其有意义... - Yuval Adam
7
它的意思足够清晰,得到了一个明智的回答。 - Ed Guiness
嗨,我的问题是: 如果可以的话,我能否将属性作为“out”或“ref”参数传递?如果不行,为什么? 例如。 Person p = new Person(); public void Test(out p.Name); - Embedd_0913
3个回答

43
抱歉回答简短,但是不行,C#语言规范禁止这样做。请参阅这个问题的答案以了解尝试时会发生什么。它还说明了为什么你不应该使属性成为公共字段来绕过限制。希望这可以帮到你。
编辑:你问为什么?
当你将一个变量传递给outref参数时,实际上是传递变量的地址(或内存位置)。在函数内部,编译器知道变量的真实位置,并获取并写入该地址的值。
属性看起来像一个值,但实际上是一对具有不同签名的函数。因此,要传递属性,您实际上需要传递两个函数指针,一个用于获取,另一个用于设置。
这与传递变量地址完全不同,即一个变量地址v与两个函数指针。
更新 C#为什么不替我们处理这个问题?
我不是Eric Lippert,但我会试着解释为什么。

你调用的函数应该具有什么样的签名?
假设你想调用void MyFn(ref int i),那么它是否应该保持原样,还是应该改为允许属性?如果更改为void MyFn(prop_ref int i)这样的语法,则相当无用,因为您不能将属性传递给未使用特殊prop_ref修饰符编写的库函数或第三方代码。无论如何,我认为您建议不应该有所不同。

现在假设MyFni传递给COM函数或WinAPI调用,通过引用(即在.net之外)。如果它是一个属性,那么你怎么获取i的地址?也许没有实际的int类型的值可以获取地址。你会像VB.Net一样做吗?

Vb.Net编译器会在将属性作为ByRef参数传递给方法时进行检测。此时,它声明一个变量,将属性复制到变量中,通过引用传递变量,然后在调用方法后,将变量复制回属性中。例如:

MyFunc(myObject.IntProperty)

成为

Dim temp_i As Integer = myObject.IntProperty
MyFunc(temp_i)
myObject.IntProperty = temp_i

任何属性的副作用都不会在MyFunc返回之前发生,这可能会导致各种问题并引起非常微妙的错误。
在我看来,Vb.Net解决此问题的方法也是有缺陷的,因此我不会接受它作为答案。
您认为C#编译器应该如何处理这个问题?

23
这个有问题。C#不是低级语言,因此“ref”参数不应该是一个地址,而应该是一对闭包,即一个getter和一个setter。如果将本地变量作为“ref”参数传递,则语言应自动创建getter/setter对。所有这些都应该对毫无戒心的程序员隐藏起来。 - isekaijin
1
@EduardoLeón:有很多事情可以通过地址来完成,而闭包无法做到。允许将属性作为引用传递的正确方式是定义一种特殊的属性,它不仅包含getter和setter,还以受控的方式公开一个地址(例如,类型为T的属性可以有一个方法,该方法接受一个接受ref参数类型为T的委托,并将该属性的后备字段(或其他类型为T的存储位置)的地址传递给该委托)。 - supercat
1
一个属性是严格等同于一个get/set函数对。这就是它的本质。因此(如@EduardoLeón所指出的),关于它的一切有用信息都可以通过两个闭包来捕获。我在Roslyn中尝试了这样的实现:https://smellegantcode.wordpress.com/2014/04/27/adventures-in-roslyn-a-new-kind-of-managed-lvalue-pointer/ - Daniel Earwicker
2
@supercat,虽然更简单的改进是让您在自动属性上使用ref。编译器会生成它们的后备字段,因此它只需使ref在后备字段上工作即可。 - Daniel Earwicker
1
@EduardoLeón:“所有这些都应该对毫无戒心的程序员隐藏起来。” 目前,ref参数保证不会产生副作用。例如,一个方法可能会在返回之前多次使用+=来附加到ref string,并在进行检查时可能会检查该值。该方法的程序员需要知道它的参数是否支持属性,因为在这种情况下,他们需要创建一个临时变量。 - hypehuman
显示剩余6条评论

15

其它人已经解释过在C#中你无法这样做。而在VB.NET中,即使使用了option strict/explicit也可以实现:

Option Strict On
Option Explicit On
Imports System.Text

Module Test

   Sub Main()
       Dim sb as new StringBuilder
       Foo (sb.Length)
   End Sub
   
   Sub Foo(ByRef x as Integer)
   End Sub
    
End Module

上述代码等价于以下C#代码:
using System.Text;

class Test
{
     static void Main()
     {
         StringBuilder sb = new StringBuilder();
         int tmp = sb.Length;
         Foo(ref tmp);
         sb.Length = tmp;
     }

     static void Foo(ref int x)
     {
     }
}

个人而言,我很高兴 C# 没有这个功能——它会让事情变得更加混乱,特别是在方法内设置参数但随后抛出异常时属性的值会产生歧义的情况下。
编辑:根据要求,我解释一下为什么我认为传递属性会让事情变得混乱。如果你通过引用传递一个普通变量,在方法内每次引用该变量时都会重新计算一次它的值。如果由于某些原因(例如方法中的其他工作的副作用)导致值发生更改,则该更改将立即在方法中可见。但是,如果你在 VB.NET 中通过引用传递一个属性,则属性 getter 会被调用一次,然后属性 setter 会被调用一次。这并不像你正在传入“这是一个 属性 —— 每当使用参数时从该属性获取和设置”。
以下是一个完整的示例,说明在 .NET 中传递字段和传递完全无意义的属性会产生非常不同的结果:
Option Strict On
Option Explicit On
Imports System.Text

Class Test

   Dim counter as Integer
   
   Property CounterProperty As Integer
       Get
           Return counter
       End Get
       Set (ByVal value as Integer)
           counter = value
       End Set
   End Property
   
   Sub Increment
       counter += 1
   End Sub

   Shared Sub Main()
       Dim t as new Test()
       Console.WriteLine("Counter = {0}", t.counter)
       t.Foo(t.counter)
       Console.WriteLine("Counter = {0}", t.counter)

       t.CounterProperty = 0
       Console.WriteLine("CounterProperty = {0}", t.CounterProperty)
       t.Foo(t.CounterProperty)
       Console.WriteLine("CounterProperty = {0}", t.CounterProperty)
   End Sub
   
   Sub Foo(ByRef x as Integer)
       x = 5
       Increment
       Increment
       Increment
       x += 1
   End Sub
    
End Class

.Net-Fiddle: https://dotnetfiddle.net/ZPFIEZ(字段和属性有不同的结果)


2
Jon:如果您不介意的话,能否阐述一下您对“混淆视听”的看法?我不明白为什么您认为将属性传递到ref参数中是一个特别糟糕的想法(我并不是说它们是一个好主意)。谢谢,伙计。 - Binary Worrier

0

相反,你应该做像这样的事情

WhatEverTheType name;

Test(out name);

// Choose one of the following construction

Person p = new Person();
p.Name = name;

Person p = new Person(name);
Person p = new Person(Name => name);

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