何时使用ref以及何时在C#中不需要


104

我有一个对象,它在程序的内存状态中,并且还有一些其他工作函数,我将该对象传递给该函数以修改状态。我一直在通过ref传递给worker函数。但是我遇到了以下功能。

byte[] received_s = new byte[2048];
IPEndPoint tmpIpEndPoint = new IPEndPoint(IPAddress.Any, UdpPort_msg);
EndPoint remoteEP = (tmpIpEndPoint);

int sz = soUdp_msg.ReceiveFrom(received_s, ref remoteEP); 

这使我感到困惑,因为received_sremoteEP都从函数返回了东西。为什么remoteEP需要一个refreceived_s不?

我也是交流程序员,所以在将指针弄出头脑时遇到了问题。

编辑:看起来C#中的对象是指向内部对象的指针。因此,当您将对象传递给函数时,可以通过指针修改对象的内容,唯一传递给该函数的是对象的指针,这样就不会复制对象本身。如果希望能够在函数中切换或创建一个类似于双指针的新对象,则使用ref或out。

Answers:


216

简短的答案:阅读有关论点传递的文章

长答案:当按值传递引用类型参数时,仅传递引用,而不传递对象的副本。这就像在C或C ++中传递指针(按值)。更改参数本身的值将不会被呼叫方可以看到,但改变其中的参考点到对象被看见。

通过引用传递(任何类型的)参数时,这意味着调用者可以看到对参数的任何更改-对参数更改就是对变量的更改。

当然,本文更详细地解释了所有这些:)

有用的答案:您几乎不需要使用ref / out。从本质上讲,这是获取另一个返回值的一种方法,通常应严格避免使用它,因为这意味着该方法可能尝试做太多事情。情况并非总是如此(TryParse等等是合理使用的规范示例out),但是使用ref / out应该是相对罕见的。


38
我认为您的短答案和长答案混在一起了。那是一篇大文章!
Outlaw程序员,2009年

23
@Outlaw:是的,但是简短的答案本身(阅读文章的指令)只有6个字长:)
Jon Skeet

27
参考!:)
gonzobrains 2012年

5
@Liam像您一样使用ref可能对您来说更加明确,但实际上可能会使其他程序员(无论如何知道该关键字的人)感到困惑,因为您实际上是在告诉潜在的调用者,“我可以修改变量,在调用方法中使用,即将其重新分配给其他对象(如果可能,甚至重新分配为null),因此请不要挂在它上面,或者确保在完成后验证它”。这非常强大,并且与“可以修改该对象”完全不同,在任何时候将对象引用作为参数传递时,情况总是如此。
mbargiel 2014年

1
@ M.Mimpen:C#7(希望)允许if (int.TryParse(text, out int value)) { ... use value here ... }
Jon Skeet

26

将非ref参数视为指针,并将ref参数视为双指针。这对我最大的帮助。

您几乎应该永远不要通过引用传递值。我怀疑如果不是出于互操作性的考虑,.Net团队将永远不会将其包含在原始规范中。处理ref参数解决的大多数问题的OO方法是:

对于多个返回值

  • 创建表示多个返回值的结构

对于因方法调用而在方法中发生变化的基元(方法对基元参数有副作用)

  • 在对象中将方法实现为实例方法,并在方法调用中处理对象的状态(而不是参数)
  • 使用多重返回值解决方案并将返回值合并到您的状态
  • 创建一个对象,该对象包含可由方法操纵的状态,并将该对象作为参数而不是原语本身传递。

8
天哪 我必须阅读20倍才能理解。听起来对我来说,要做一些简单的事情需要做很多额外的工作。
PositiveGuy 2010年

.NET框架不遵循您的#1规则。(“为多个返回值创建结构”)。举个例子IPAddress.TryParse(string, out IPAddress)
Swen Kooij 2013年

@SwenKooij你是对的。我假设在大多数使用参数的地方,它们要么(a)包装了Win32 API,要么(b)还很早,而C ++程序员正在做决定。
Michael Meadows 2014年

@SwenKooij我知道这个答复已经晚了很多年,但是确实如此。我们已经习惯了TryParse,但这并不意味着它很好。如果要比if (int.TryParse("123", out var theInt) { /* use theInt */ }我们拥有的更好,那就更好var candidate = int.TrialParse("123"); if (candidate.Parsed) { /* do something with candidate.Value */ }了。代码更多,但与C#语言设计更加一致。
Michael Meadows '18年

9

您可能会编写一个完整的C#应用​​程序,并且永远不会通过ref传递任何对象/结构。

我有一位教授告诉我这件事:

您唯一使用裁判的地方是:

  1. 想要传递一个大对象(即,对象/结构内部具有多个对象/结构,将其复制到多个级别),将其复制会很昂贵,并且
  2. 您正在调用框架,Windows API或需要它的其他API。

不要仅仅因为可以就这样做。如果您开始更改参数中的值并且不注意,则可能会遇到一些讨厌的错误,从而使您陷入困境。

我同意他的建议,并且自从上学以来的五年多时间里,除了调用Framework或Windows API外,我从不需要它。


3
如果您计划实现“交换”,则通过ref传递可能会有所帮助。
布赖恩2009年

如果我对小对象使用ref关键字,@ Chris会出现任何问题吗?
ManirajSS 2014年

@TechnikEmpire“但是,在调用函数范围内对对象的更改不会反映回调用者。” 那是错的。如果我将Person传递给SetName(person, "John Doe"),则name属性将更改,并且该更改将反映给调用方。
M. Mimpen '16

@ M.Mimpen评论已删除。那时我几乎没有拿起C#,显然是在说我的* $$。感谢您引起我的注意。

@Chris-我很确定传递一个“大”对象并不昂贵。如果仅按值传递它,那么您仍然只是传递单个指针,对吗?
伊恩

3

由于receive_s是一个数组,因此您要传递一个指向该数组的指针。该函数在适当的位置操作现有数据,而不更改基础位置或指针。ref关键字表示您正在将实际指针传递到该位置并在外部函数中更新该指针,因此外部函数中的值将更改。

例如,字节数组是指向刚刚更新的内存之前和之后的相同内存的指针。

端点引用实际上是将外部函数中指向端点的指针更新为该函数内部生成的新实例。


3

认为ref意味着您正在按引用传递指针。不使用ref表示您正在按值传递指针。

更好的是,请忽略我刚才所说的内容(这可能会产生误导,尤其是对于值类型而言),并阅读This MSDN page


其实不是这样。至少第二部分。无论是否使用ref,任何引用类型都将始终通过引用传递。
Erik Funkenbusch 09年

实际上,经过进一步的思考,这并不正确。引用实际上是按值传递的,而没有ref类型。意思是,更改参考所指向的值会更改原始数据,但是更改参考本身不会更改原始参考。
Erik Funkenbusch,2009年

2
非引用引用类型不会通过引用传递。对引用类型的引用按值传递。但是,如果您想将引用视为指向所引用对象的指针,那么我所说的话是有道理的(但是以这种方式思考可能会产生误导)。因此,我的警告。
布赖恩2009年


0

我的理解是,所有从Object类派生的对象都作为指针传递,而普通类型(int,struct)不作为指针传递并且需要引用。我不确定字符串(它最终是从Object类派生的吗?)


这可能需要一些澄清。价值基准和参考基准之间的区别是什么。Ans为什么与在参数上使用ref关键字相关?
2012年

在.net中,除指针,类型参数和接口以外的所有内容均源自Object。重要的是要理解,“普通”类型(正确称为“值类型”)也从对象继承。您是正确的:值类型(默认情况下)按值传递,而引用类型按引用传递。如果要修改一个值类型,而不是返回一个新的值类型(像在方法内部的引用类型一样处理它),则必须在其上使用ref关键字。但这是不好的风格,除非您绝对确定必须这样做,否则您不应该这样做:)
buddybubble

1
回答您的问题:字符串是从object派生的。它是一种引用类型,其行为类似于值类型(出于性能和逻辑原因)
buddybubble 2013年

0

尽管我完全同意Jon Skeet的答案以及其他一些答案,但仍有一个使用案例ref,用于加强性能优化。在性能分析过程中已经观察到,设置方法的返回值对性能有轻微的影响,而使用ref参数作为返回值填充到该参数中的参数导致消除了该瓶颈。

这仅在优化工作达到极限水平,牺牲可读性以及可能的可测试性和可维护性以节省毫秒或什至毫秒级的情况下才有用。


-1

首先是零基础规则,在涉及的TYPES上下文中,按值(堆栈)传递基元,按引用(堆)传递非原语。

默认情况下,所涉及的参数由Value传递。好帖子,详细解释了事情。 http://yoda.arachsys.com/csharp/parameters.html

Student myStudent = new Student {Name="A",RollNo=1};

ChangeName(myStudent);

static void ChangeName(Student s1)
{
  s1.Name = "Z"; // myStudent.Name will also change from A to Z
                // {AS s1 and myStudent both refers to same Heap(Memory)
                //Student being the non-Primitive type
}

ChangeNameVersion2(ref myStudent);
static void ChangeNameVersion2(ref Student s1)
{
  s1.Name = "Z"; // Not any difference {same as **ChangeName**}
}

static void ChangeNameVersion3(ref Student s1)
{
    s1 = new Student{Name="Champ"};

    // reference(myStudent) will also point toward this new Object having new memory
    // previous mystudent memory will be released as it is not pointed by any object
}

我们可以说(警告)非基本类型不过是指针而已 ,当我们通过ref传递它们时,可以说我们正在传递Double Pointer


默认情况下,所有参数都在C#中按值传递。 任何参数都可以通过引用传递。对于引用类型,传递的值(通过引用或通过值)本身就是引用。这完全独立于它的传递方式。
Servy 2014年

同意@Servy!“当我们听到使用“引用”或“值”这两个词时,我们应该非常清楚自己是要表示参数是引用还是值参数,或者是要表示所涉及的类型是引用还是值类型。我这边很困惑,我先说“零地面规则”,基元是通过值(堆栈)传递的,而非原始是通过引用(堆)传递的,我的意思是所涉及的类型,而不是参数!当我们谈论参数时,您是正确的,所有参数都通过值在C#中传递,在默认情况下。
Surender辛格·马利克

说参数是引用还是值参数不是标准术语,并且对于您的意思确实是模棱两可的。参数的类型可以是引用类型或值类型。任何参数都可以按值或引用传递。这些是任何给定参数的正交概念。您的帖子错误地描述了这一点。
Servy 2014年

1
您可以按引用或按值传递参数。术语“按引用”和“按值”不用于描述类型是值类型还是引用类型。
Servy 2014年

1
不,这不是感知问题。当您使用不正确的术语来指代某物时,该陈述将变为错误。对于正确的陈述而言,使用正确的术语来引用概念非常重要。
Servy 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.