“ref”和“out”关键字有什么区别?

我正在创建一个函数,我需要在其中传递一个对象,以便它可以被函数修改。有什么区别:

public void myFunction(ref MyClass someClass)

public void myFunction(out MyClass someClass)

我应该使用什么,为什么?

420193 次浏览

ref是inout

您应该优先使用out,只要它足以满足您的要求。

ref告诉编译器对象在进入函数之前被初始化,而out告诉编译器对象将在函数内部初始化。

所以ref是双向的,out是外向的。

由于您传递的是引用类型(类),因此不需要使用ref,因为默认情况下只传递参考到实际对象,因此您总是更改引用后面的对象。

示例:

public void Foo(){MyClass myObject = new MyClass();myObject.Name = "Dog";Bar(myObject);Console.WriteLine(myObject.Name); // Writes "Cat".}
public void Bar(MyClass someObject){someObject.Name = "Cat";}

只要您传入一个类,如果您想更改方法中的对象,您就不必使用ref

ref修饰符意味着:

  1. 该值已设置并且
  2. 该方法可以读取和修改它。

out修饰符意味着:

  1. 值未设置,并且无法由设置它的方法直到读取。
  2. 方法必须在返回之前设置它。

“贝克”

这是因为第一个将您的string-引用更改为指向“Baker”。更改引用是可能的,因为您通过ref关键字(=>对字符串引用的引用)传递了它。第二个调用获取对字符串的引用的副本。

字符串一开始看起来有些特别。但字符串只是一个引用类,如果您定义

string s = "Able";

则s是对包含文本“Able”的字符串类的引用!通过

对同一个变量进行另一个赋值
s = "Baker";

不会更改原始字符串,只是创建一个新实例,让我们指向该实例!

您可以使用以下小代码示例进行尝试:

string s = "Able";string s2 = s;s = "Baker";Console.WriteLine(s2);

你想要什么?您将获得的仍然是“Able”,因为您只是将s中的引用设置为另一个实例,而s2指向原始实例。

编辑:字符串也是不可变的,这意味着根本没有方法或属性可以修改现有的字符串实例(你可以尝试在文档中找到一个,但你不会找到任何 :-) ). 所有字符串操作方法都返回一个新的字符串实例!(这就是为什么你在使用StringBuilder类时经常得到更好的性能)

假设多姆出现在彼得的隔间里,关于TPS报告的备忘录。

如果Dom是一个裁判论点,他会有一份备忘录的打印副本。

如果多姆是一个公开的论点,他会让彼得打印一份新的备忘录副本,让他随身携带。

请注意,在函数内部传递的引用参数是直接处理的。

例如,

    public class MyClass{public string Name { get; set; }}
public void Foo(){MyClass myObject = new MyClass();myObject.Name = "Dog";Bar(myObject);Console.WriteLine(myObject.Name); // Writes "Dog".}
public void Bar(MyClass someObject){MyClass myTempObject = new MyClass();myTempObject.Name = "Cat";someObject = myTempObject;}

这将写狗,而不是猫。因此,您应该直接处理某些对象。

扩展狗,猫示例。带有ref的第二个方法更改调用者引用的对象。因此“Cat”!!!

    public static void Foo(){MyClass myObject = new MyClass();myObject.Name = "Dog";Bar(myObject);Console.WriteLine(myObject.Name); // Writes "Dog".Bar(ref myObject);Console.WriteLine(myObject.Name); // Writes "Cat".}
public static void Bar(MyClass someObject){MyClass myTempObject = new MyClass();myTempObject.Name = "Cat";someObject = myTempObject;}
public static void Bar(ref MyClass someObject){MyClass myTempObject = new MyClass();myTempObject.Name = "Cat";someObject = myTempObject;}

我将尝试我的手在一个解释:

我想我们理解了值类型是如何工作的?值类型是(int、long、struct等)。当你在没有ref命令的情况下将它们发送到函数时,它会复制数据。你对函数中的数据所做的任何事情都只影响副本,而不影响原始数据。ref命令发送ACTUAL数据,任何更改都将影响函数之外的数据。

好的,进入令人困惑的部分,引用类型:

让我们创建一个引用类型:

List<string> someobject = new List<string>()

当你新建某个物体时,会创建两个部分:

  1. 存储某个物体数据的内存块。
  2. 指向该块的引用(指针)的数据。

现在,当你将某个物体发送到一个没有引用的方法时,它复制了参考指针,而不是数据。所以你现在有了这个:

(outside method) reference1 => someobject(inside method)  reference2 => someobject

两个引用指向同一个对象。如果您使用引用2修改某个物体上的属性,它将影响引用1指向的相同数据。

 (inside method)  reference2.Add("SomeString");(outside method) reference1[0] == "SomeString"   //this is true

如果您将引用2清空或将其指向新数据,它不会影响引用1,也不会影响引用1指向的数据。

(inside method) reference2 = new List<string>();(outside method) reference1 != null; reference1[0] == "SomeString" //this is true
The references are now pointing like this:reference2 => new List<string>()reference1 => someobject

现在,当您通过ref将某个物体发送给方法时会发生什么?实际参考某个物体被发送到方法。所以你现在只有一个对数据的引用:

(outside method) reference1 => someobject;(inside method)  reference1 => someobject;

但是这是什么意思?它的行为与不通过ref发送某个对象完全相同,除了两个主要的事情:

1)当您将方法内部的引用清空时,它将清空方法外部的引用。

 (inside method)  reference1 = null;(outside method) reference1 == null;  //true

2)您现在可以将引用指向完全不同的数据位置,函数外部的引用现在将指向新的数据位置。

 (inside method)  reference1 = new List<string>();(outside method) reference1.Count == 0; //this is true

我可能不太擅长这个,但肯定字符串(即使它们在技术上是引用类型并且存在于堆上)是按值传递的,而不是引用?

        string a = "Hello";
string b = "goodbye";
b = a; //attempt to make b point to a, won't work.
a = "testing";
Console.WriteLine(b); //this will produce "hello", NOT "testing"!!!!

这就是为什么你需要ref,如果你希望更改存在于函数作用域之外,否则你不会传递引用。

据我所知,你只需要ref结构/值类型和字符串本身,因为字符串是一个引用类型,假装它是但不是一个值类型。

我可能完全错了,我是新来的。

它们几乎相同——唯一的区别是,作为out参数传递的变量不需要初始化,使用ref参数的方法必须将其设置为某个值。

int x;    Foo(out x); // OKint y;    Foo(ref y); // Error

ref参数用于可能被修改的数据,out参数用于已经使用返回值的函数(例如int. TryParse)的附加输出数据。

下面我展示了一个同时使用参考的例子。现在,你们都将被清除关于ref和out的内容。

在下面提到的例子中,当我评论//myRefObj=new myClass{Name="ref外面叫!!"};行,会得到一个错误说"使用未赋值的局部变量'myRefObj'",但在中没有这样的错误。

在哪里使用Ref:当我们使用in参数调用过程时,相同的参数将用于存储该过程的输出。

在哪里使用:当我们调用一个没有参数的过程时,相同的参数将用于从该过程返回值。另请注意输出

public partial class refAndOutUse : System.Web.UI.Page{protected void Page_Load(object sender, EventArgs e){myClass myRefObj;myRefObj = new myClass { Name = "ref outside called!!  <br/>" };myRefFunction(ref myRefObj);Response.Write(myRefObj.Name); //ref inside function
myClass myOutObj;myOutFunction(out myOutObj);Response.Write(myOutObj.Name); //out inside function}
void myRefFunction(ref myClass refObj){refObj.Name = "ref inside function <br/>";Response.Write(refObj.Name); //ref inside function}void myOutFunction(out myClass outObj){outObj = new myClass { Name = "out inside function <br/>" };Response.Write(outObj.Name); //out inside function}}
public class myClass{public string Name { get; set; }}

refout的行为类似,除了以下差异。

  • ref变量必须在使用前初始化。out变量可以在没有赋值的情况下使用
  • out参数必须被使用它的函数视为未赋值。因此,我们可以在调用代码中使用初始化的out参数,但当函数执行时,该值将丢失。

<强>出:返回语句只能用于从函数返回一个值。但是,使用输出参数,您可以从函数返回两个值。输出参数类似于引用参数,只是它们将数据传输出方法而不是传输到方法中。

下面的例子说明了这一点:

using System;
namespace CalculatorApplication{class NumberManipulator{public void getValue(out int x ){int temp = 5;x = temp;}
static void Main(string[] args){NumberManipulator n = new NumberManipulator();/* local variable definition */int a = 100;
Console.WriteLine("Before method call, value of a : {0}", a);
/* calling a function to get the value */n.getValue(out a);
Console.WriteLine("After method call, value of a : {0}", a);Console.ReadLine();
}}}

ref:引用参数是对变量内存位置的引用。当您通过引用传递参数时,与值参数不同,不会为这些参数创建新的存储位置。引用参数表示与提供给方法的实际参数相同的内存位置。

在C#中,您可以使用ref关键字声明引用参数。以下示例演示了这一点:

using System;namespace CalculatorApplication{class NumberManipulator{public void swap(ref int x, ref int y){int temp;
temp = x; /* save the value of x */x = y;   /* put y into x */y = temp; /* put temp into y */}
static void Main(string[] args){NumberManipulator n = new NumberManipulator();/* local variable definition */int a = 100;int b = 200;
Console.WriteLine("Before swap, value of a : {0}", a);Console.WriteLine("Before swap, value of b : {0}", b);
/* calling a function to swap the values */n.swap(ref a, ref b);
Console.WriteLine("After swap, value of a : {0}", a);Console.WriteLine("After swap, value of b : {0}", b);
Console.ReadLine();
}}}
 public static void Main(string[] args){//int a=10;//change(ref a);//Console.WriteLine(a);// Console.Read();
int b;change2(out b);Console.WriteLine(b);Console.Read();}// static void change(ref int a)//{//    a = 20;//}
static void change2(out int b){b = 20;}

您可以检查此代码,它会描述您的完全不同当你使用“ref”时,它意味着你已经初始化了那个int/string

但是当你使用“out”时它适用于两种情况,无论你是否初始化该int/string但是你必须在那个函数中初始化那个int/string

参考表示ref参数中的值已经设置,该方法可以读取和修改它。使用ref关键字与说调用者负责初始化参数的值是一样的。


告诉编译器对象的初始化是由函数,该函数必须分配给out参数。不允许未分配它。

https://www.codemaggot.com/ref-and-out-keywords/

从接收参数的方法的角度来看,refout的区别在于C#要求方法在返回之前必须写入每个out参数,并且除了将其作为out参数传递或写入之外,不得对此类参数做任何事情,直到它作为out参数传递给另一个方法或直接写入。请注意,一些其他语言没有强加此类要求;在C#中声明带有out参数的虚拟或接口方法可能会被另一种对此类参数没有任何特殊限制的语言覆盖。

从调用者的角度来看,C#在许多情况下会假设当调用带有out参数的方法时,会导致传递的变量在没有被读取的情况下被写入。当调用用其他语言编写的方法时,这个假设可能不正确。例如:

struct MyStruct{...myStruct(IDictionary<int, MyStruct> d){d.TryGetValue(23, out this);}}

如果myDictionary标识了用C#以外的语言编写的IDictionary<TKey,TValue>实现,即使MyStruct s = new MyStruct(myDictionary);看起来像一个赋值,它也可能会使s未修改。

请注意,用VB.NET编写的构造函数与C#中的构造函数不同,它不假设被调用的方法是否会修改任何out参数,并无条件地清除所有字段。上面提到的奇怪行为不会发生在完全用VB或完全用C#编写的代码中,但当用C#编写的代码调用用VB.NET.编写的方法时会发生

ref和out的工作就像C++中的引用和指针传递一样。

对于ref,参数必须声明并初始化。

对于out,参数必须声明,但可以初始化也可以不初始化

        double nbr = 6; // if not initialized we get errordouble dd = doit.square(ref nbr);
double Half_nbr ; // fine as passed by out, but inside the calling  method you initialize itdoit.math_routines(nbr, out Half_nbr);

参考:ref关键字用于传递参数作为引用。这意味着当该参数的值在方法中更改时,它会反映在调用方法中。使用ref关键字传递的参数必须在调用方法中初始化,然后才能传递给被调用的方法。

输出:out关键字也用于传递参数,例如ref关键字,但可以在不为其分配任何值的情况下传递参数。使用out关键字传递的参数必须在被调用方法中初始化,然后才能返回调用方法。

public class Example{public static void Main(){int val1 = 0; //must be initializedint val2; //optional
Example1(ref val1);Console.WriteLine(val1);
Example2(out val2);Console.WriteLine(val2);}
static void Example1(ref int value){value = 1;}static void Example2(out int value){value = 2;}}
/* Output     1     2

方法重载中的Ref和out

ref和out不能同时用于方法重载。但是,ref和out在运行时被区别对待,但在编译时被区别对待(CLR在为ref和out创建IL时不会区分两者)。

对于那些通过例子学习的人(像我一样),这里是安东尼·科列索夫说

我已经创建了一些ref、out和其他示例来说明这一点。我不是在介绍最佳实践,只是举例来理解差异。

https://gist.github.com/2upmedia/6d98a57b68d849ee7091

出:

在C#中,一个方法只能返回一个值。如果你想返回多个值,你可以使用out关键字。out修饰符作为引用返回。最简单的答案是关键字“out”用于从方法中获取值。

  1. 您不需要初始化调用函数中的值。
  2. 您必须在被调用的函数中分配值,否则编译器将报告错误。

参考:

在C#中,当你将一个值类型,如int、浮点数、双精度等作为参数传递给方法参数时,它是按值传递的。因此,如果你修改参数值,它不会影响方法调用中的参数。但是如果你用“ref”关键字标记参数,它会反映在实际变量中。

  1. 在调用函数之前,您需要初始化变量。
  2. 给方法中的ref参数赋值不是强制性的。如果不更改值,为什么需要将其标记为“ref”?

如果你想把你的参数作为参考传递,那么你应该在把参数传递给函数之前初始化它,否则编译器本身会显示错误。但是在out参数的情况下,你不需要在把对象参数传递给方法之前初始化它。你可以在调用方法本身初始化对象。

创作时间:

(1)我们创建调用方法Main()

(2)它创建一个List对象(它是一个引用类型的对象)并将其存储在变量myList中。

public sealed class Program{public static Main(){List<int> myList = new List<int>();

运行时:

(3)运行时在堆栈上分配内存myList0,足够宽以存储地址(myList0=myList,因为变量名实际上只是内存位置的别名)

(4)运行时在内存位置#FF的堆上创建一个列表对象(所有这些地址都是示例)

(5)然后运行时将对象的起始地址#FF存储在myList0(或者用单词来说,将List对象的引用存储在指针myList中)

回到创作时间:

(6)然后我们将List对象作为参数myParamList传递给被调用的方法modifyMyList,并为其分配一个新的List对象

List<int> myList = new List<int>();
List<int> newList = ModifyMyList(myList)
public List<int> ModifyMyList(List<int> myParamList){myParamList = new List<int>();return myParamList;}

运行时:

(7)运行时启动被调用方法的调用例程,并作为其中的一部分,检查参数的类型。

(8)找到引用类型后,它在堆栈上分配内存myParamList4,以防止参数变量myParamList出现混淆现象。

(9)然后它也将值#FF存储在其中。

(10)运行时在内存位置#004的堆上创建一个列表对象,并将#04中的#FF替换为此值(或取消引用原始List对象并指向此方法中的新List对象)

myList0中的地址不会改变,并保留对#FF的引用(或者原始myList指针不会受到干扰)。


ref关键字是一个编译器指令,用于跳过为(8)和(9)生成运行时代码,这意味着方法参数将没有堆分配。它将使用原始ref0指针对#FF处的对象进行操作。如果原始指针未初始化,运行时将停止抱怨由于变量未初始化而无法继续

<强>出关键字是一个编译器指令,与ref几乎相同,在(9)和(10)处略有修改。编译器希望参数未初始化,并将继续使用(8)、(4)和(5)在堆上创建一个对象并将其起始地址存储在参数变量中。不会抛出未初始化的错误,并且存储的任何先前引用都将丢失。

除了允许您将其他人的变量重新分配给类的不同实例,返回多个值等,使用#0或#1可以让其他人知道你需要从他们那里得到什么,以及你打算用他们提供的变量做什么

  • 不需要refout,如果您要做的只是修改里面参数someClass中传递的MyClass实例。

    • 调用方法将看到someClass.Message = "Hello World"之类的更改,无论您使用refout还是不使用
    • myFunction(someClass)中写入someClass = new MyClass()仅在myFunction方法的范围内交换someClass看到的对象。调用方法仍然知道它创建并传递给您的方法的原始MyClass实例
  • 如果您计划将someClass交换为一个全新的对象并希望调用方法看到您的更改,您将需要refout

    • myFunction(out someClass)中写入someClass = new MyClass()会改变调用myFunction的方法看到的对象

其他程序员存在

他们想知道你将如何处理他们的数据。假设你正在编写一个将被数百万开发人员使用的库。当他们调用你的方法时,你希望他们知道你将如何处理他们的变量

  • 使用ref声明“当您调用我的方法时,传递一个分配给某个值的变量。请注意,我可能会在方法过程中完全将其更改为其他内容。当我完成时,不要期望您的变量指向旧对象”

  • 使用out声明“将占位符变量传递给我的方法。它是否有值并不重要;编译器会强制我将其分配给一个新值。我绝对保证在调用我的方法之前,您的变量指向的对象将在我完成时不同

顺便说一句,在C#7.2中也有一个in修饰符

这会阻止方法将传入的实例替换为其他实例。把它想象成对数百万开发人员说“把你的原始变量引用传递给我,我保证不会把你精心制作的数据换成其他东西”。in有一些特点,在某些情况下,比如可能需要隐式转换才能使你的短与in int兼容,编译器会暂时制作一个int,将你的短扩展到它,通过引用传递它并完成。它可以做到这一点,因为你已经声明你不会弄乱它。


Microsoft在数字类型上使用.TryParse方法做到了这一点:

int i = 98234957;bool success = int.TryParse("123", out i);

通过将参数标记为out,他们在这里积极声明“我们绝对将把您精心制作的98234957值更改为其他值”

当然,他们不得不这样做,对于解析值类型之类的事情,因为如果不允许parse方法将值类型换成其他类型,它就不会很好地工作…但是想象一下,在你正在创建的某个库中有一些虚构的方法:

public void PoorlyNamedMethod(out SomeClass x)

你可以看到它是一个out,因此你可以知道,如果你花几个小时处理数字,创建完美的某个类:

SomeClass x = SpendHoursMakingMeAPerfectSomeClass();//now give it to the libraryPoorlyNamedMethod(out x);

好吧,那是浪费时间,花那么多时间来制作完美的类。它肯定会被丢弃,取而代之的是PoorlyNamedLaw

为了说明许多优秀的解释,我开发了以下控制台应用程序:

using System;using System.Collections.Generic;
namespace CSharpDemos{class Program{static void Main(string[] args){List<string> StringList = new List<string> { "Hello" };List<string> StringListRef = new List<string> { "Hallo" };
AppendWorld(StringList);Console.WriteLine(StringList[0] + StringList[1]);
HalloWelt(ref StringListRef);Console.WriteLine(StringListRef[0] + StringListRef[1]);
CiaoMondo(out List<string> StringListOut);Console.WriteLine(StringListOut[0] + StringListOut[1]);}
static void AppendWorld(List<string> LiStri){LiStri.Add(" World!");LiStri = new List<string> { "¡Hola", " Mundo!" };Console.WriteLine(LiStri[0] + LiStri[1]);}
static void HalloWelt(ref List<string> LiStriRef){ LiStriRef = new List<string> { LiStriRef[0], " Welt!" }; }
static void CiaoMondo(out List<string> LiStriOut){ LiStriOut = new List<string> { "Ciao", " Mondo!" }; }}}/*Output:¡Hola Mundo!Hello World!Hallo Welt!Ciao Mondo!*/
  • AppendWorld:传递了名为LiStriStringList副本。在方法开始时,此副本引用原始列表并因此可用于修改此列表。后来的LiStri引用方法中另一个List<string>对象,不影响原始列表。

  • HalloWeltLiStriRef是已初始化的对象的别名ListStringRef。传递的List<string>对象用于初始化所以ref是必须的。

  • CiaoMondoLiStriOutListStringOut的别名,必须是初始化。

因此,如果一个方法只是修改了传递的变量引用的对象,编译器不会让你使用out,你也不应该使用ref,因为它不会混淆编译器,而是混淆代码的读者。如果该方法将使传递的参数引用另一个对象,请对已经初始化的对象使用ref,对必须为传递的参数初始化新对象的方法使用out。除此之外,refout的行为相同。

对于那些寻找简洁的答案。

refout关键字都用于传递reference


ref关键字的变量必须有一个值或必须引用一个对象或者null之前它的传递。


ref不同,out关键字的变量必须有一个值或必须引用一个对象或null之后它的传递以及不需要有一个值或引用一个对象之前传递。

有两个主要的区别,我想举个例子:

  1. refout引用传递,hense;
 class Program{public static void Main(string[] args){var original = new ObjectWithMememberList(3);Console.WriteLine(original.MyList.Capacity); // 3ChangeList(original.MyList);Console.WriteLine(original.MyList.Capacity); // 3}
static void ChangeList(List<int> vr){vr = new List<int>(2);}}

但是:

 class Program{public static void Main(string[] args){var original = new ObjectWithMememberList(3);Console.WriteLine(original.MyList.Capacity); // 3ChangeList(ref original.MyList);Console.WriteLine(original.MyList.Capacity); // 2}
static void ChangeList(ref List<int> vr){vr = new List<int>(2);}}

out相同。2.ref参数必须是可赋值变量。hense:

ChangeList(ref new List<int>()); // Compile Error [might not be initialized before accessing]

但是:

List<int> xs;ChangeList(out xs); // Compiles

回答晚了,但想到发帖。可能比其他答案更清楚一点。

ref关键字:

ref是一个关键字,用于通过引用传递任何值(参考编程中的按值调用和按引用调用以获取更多知识)。简而言之,你声明并初始化一个值,例如让我们说int age = 5;,这样这个年龄通过保存4个字节的位置保存在内存中。现在,如果你使用ref将这个年龄变量传递给另一个方法(这意味着通过引用而不是通过值传递它们),那么编译器将只传递该变量的引用,或者明确地说,传递存储变量的地方的内存地址,被调用的方法接收这个地址并直接访问该地址中的数据。因此,显然对该数据的任何更改也会发生在调用方法中存在的变量上。

示例:我给出我的stackoverflow帐户的密码并告诉他他可以做任何他想做的事,他可以问问题或回答问题是,他所做的任何更改都会直接影响我的帐户。

out关键字:

out和in类似于它们都传递变量的引用。现在我们知道都需要传递变量的引用,很明显内存中必须存在一个保存变量字节的地方。但在out的情况下不初始化。因为要求是,被调用的方法必须初始化值并返回它。

示例:我将stackoverflow站点地址发送给我的朋友并询问让他为我创建一个帐户并返回凭据。

关键字中:

现在in关键字的工作方式与ref关键字完全相同,只有一个条件,即作为引用传递的值不得修改。

示例:我提供了我的stackoverflow帐户的密码,但告诉他不要做任何事情,除了阅读或浏览网站。不要问任何问题,没有答案,没有投票什么都没有…

MSDN参考:

  1. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref
  2. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-parameter-modifier
  3. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/in-parameter-modifier

希望上面的内容是清楚的。