前几天有人问我应该使用parameter关键字out
而不是ref
。尽管我(我认为)了解了ref
和out
关键字(之前已经问过)之间的区别,并且最好的解释似乎是ref
== in
和out
,但是我应该始终使用out
而不是一些(假设的或代码的)示例ref
。
由于ref
更笼统,您为什么要使用out
?只是语法糖吗?
ref
是输入/输出,而out
参数是仅输出。
out
变量必须在函数中分配。
前几天有人问我应该使用parameter关键字out
而不是ref
。尽管我(我认为)了解了ref
和out
关键字(之前已经问过)之间的区别,并且最好的解释似乎是ref
== in
和out
,但是我应该始终使用out
而不是一些(假设的或代码的)示例ref
。
由于ref
更笼统,您为什么要使用out
?只是语法糖吗?
ref
是输入/输出,而out
参数是仅输出。
out
变量必须在函数中分配。
Answers:
out
除非需要,否则应使用ref
。
当需要将数据编组到例如另一个过程时,这会产生很大的不同,这可能会导致成本高昂。因此,您要避免在方法不使用初始值时将其编组。
除此之外,它还向声明或调用的读者显示初始值是相关的(并可能保留)还是被丢弃。
稍有不同,无需初始化out参数。
示例out
:
string a, b;
person.GetBothNames(out a, out b);
其中GetBothNames是一种原子检索两个值的方法,无论a和b是什么,该方法都不会改变行为。如果呼叫转到夏威夷的服务器,则将初始值从此处复制到夏威夷会浪费带宽。使用ref的类似片段:
string a = String.Empty, b = String.Empty;
person.GetBothNames(ref a, ref b);
可能会使读者感到困惑,因为看起来a和b的初始值是相关的(尽管方法名称表明它们并不相关)。
示例ref
:
string name = textbox.Text;
bool didModify = validator.SuggestValidName(ref name);
此处的初始值与方法有关。
ref
默认值。
out
参数,在主叫方法需要该方法返回到之前分配一个值。-你不具备做一个ref参数什么。
用完表示该参数未使用,仅设置。这有助于调用者了解您始终在初始化参数。
同样,ref和out不仅适用于值类型。它们还允许您重置方法中引用类型所引用的对象。
out
参数在进入函数时被视为未分配。在您首先明确分配了一些值之前,您将无法检查它们的值-根本无法使用调用函数时参数所具有的值。
在语义上,您同时ref
提供“输入”和“输出”功能,而out
仅提供“输出”功能,这是正确的。有一些事情要考虑:
out
要求接受参数的方法务必在返回之前的某个时候为变量分配一个值。您可以在某些键/值数据存储类(如Dictionary<K,V>
)中找到此模式,其中您具有的功能TryGetValue
。该函数采用一个out
参数,该参数保存了检索到的值。这是没有意义的调用者传递一个值进入此功能,所以out
被用来保证一定的价值会在通话结束后的变量,即使它是不是“真正”的数据(在的情况下TryGetValue
,其中密钥不存在)。out
ref
处理互操作代码时,参数和参数的编组方式有所不同另外,重要的是要注意,尽管引用类型和值类型在其值的性质上有所不同,但应用程序中的每个变量都指向一个保存值的内存位置,即使对于引用类型也是如此。碰巧的是,对于引用类型,包含在该内存位置中的值是另一个内存位置。当您将值传递给函数(或执行任何其他变量赋值)时,该变量的值将被复制到另一个变量中。对于值类型,这意味着将复制该类型的全部内容。对于引用类型,这意味着将复制内存位置。无论哪种方式,它都会创建变量中包含的数据的副本。它所保持的唯一真正相关性涉及赋值语义。在分配变量或按值传递(默认值)时,对原始(或新)变量进行新赋值时,不会影响其他变量。对于引用类型,是的,对实例进行的更改两侧都有可用,但这是因为实际变量只是指向另一个内存位置的指针;变量的内容(内存位置)实际上并没有改变。
传递ref
关键字表示原始变量和函数参数实际上都指向相同的内存位置。同样,这仅影响分配语义。如果将新值分配给其中一个变量,则由于其他变量指向相同的存储位置,因此新值将反映在另一侧。
TryGetValue
方法,ref
而不是out
在找不到密钥的情况下不明确使用。
这取决于编译上下文(请参见下面的示例)。
out
和ref
两者都表示变量通过引用传递,但ref
需要被传递之前被初始化变量,其可以是在编组的上下文中一个重要的区别(互操作:UmanagedToManagedTransition或反之亦然)
不要将通过引用传递的概念与引用类型的概念混淆。这两个概念并不相同。可以通过ref修改方法参数,而不管它是值类型还是引用类型。通过引用传递值类型时,没有装箱。
从官方的MSDN文档:
out
:out关键字使参数通过引用传递。这类似于ref关键字,除了ref要求在传递变量之前先对其进行初始化
ref
:ref关键字使参数按引用而不是按值传递。通过引用传递的效果是,对方法中参数的任何更改都会反映在调用方法中的基础参数变量中。引用参数的值始终与基础参数变量的值相同。
当分配参数时,我们可以验证out和ref确实相同:
CIL示例:
考虑下面的例子
static class outRefTest{
public static int myfunc(int x){x=0; return x; }
public static void myfuncOut(out int x){x=0;}
public static void myfuncRef(ref int x){x=0;}
public static void myfuncRefEmpty(ref int x){}
// Define other methods and classes here
}
在CIL,所述指令myfuncOut
和myfuncRef
如预期是相同的。
outRefTest.myfunc:
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: starg.s 00
IL_0004: ldarg.0
IL_0005: stloc.0
IL_0006: br.s IL_0008
IL_0008: ldloc.0
IL_0009: ret
outRefTest.myfuncOut:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldc.i4.0
IL_0003: stind.i4
IL_0004: ret
outRefTest.myfuncRef:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldc.i4.0
IL_0003: stind.i4
IL_0004: ret
outRefTest.myfuncRefEmpty:
IL_0000: nop
IL_0001: ret
nop:无操作,ldloc:加载本地,stloc:堆栈本地,ldarg:加载参数,bs.s:分支到目标....
(请参阅:CIL指令列表)
以下是我从C#Out Vs Ref上的此代码项目文章中获得的一些注意事项
如果您是一个有视觉感的人,请观看此yourtube视频,该视频实际上演示了差异https://www.youtube.com/watch?v=lYdcY5zulXA
下图更直观地显示了差异
one-way
,two-way
此处可能会误用术语。它们实际上都是双向的,但是它们的概念行为在参数的引用和值上有所不同
ref
如果打算读取和写入参数,则需要使用。out
如果您仅打算编写,则需要使用。实际上,out
它适用于需要多个返回值或不想使用正常返回机制进行输出的情况(但这很少见)。
有语言机制可以辅助这些用例。Ref
参数必须先进行初始化,然后才能传递给方法(强调它们是可读写的),并且在给out
参数赋值之前无法读取它们,并保证已在参数末尾写入这些参数。方法(强调它们仅是写的事实)。违反这些原则会导致编译时错误。
int x;
Foo(ref x); // error: x is uninitialized
void Bar(out int x) {} // error: x was not written to
例如,int.TryParse
返回a bool
并接受一个out int
参数:
int value;
if (int.TryParse(numericString, out value))
{
/* numericString was parsed into value, now do stuff */
}
else
{
/* numericString couldn't be parsed */
}
这是需要输出两个值的清楚例子,该值是数值结果以及转换是否成功。CLR的作者决定选择out
此处,因为他们并不关心int
以前的情况。
对于ref
,您可以查看Interlocked.Increment
:
int x = 4;
Interlocked.Increment(ref x);
Interlocked.Increment
原子地增加的值x
。由于您需要阅读x
以增加它,因此这种情况ref
更为合适。您完全关心x
传递给的内容Increment
。
在C#的下一版本中,甚至可以在out
参数中声明变量,从而更加强调其仅输出特性:
if (int.TryParse(numericString, out int value))
{
// 'value' exists and was declared in the `if` statement
}
else
{
// conversion didn't work, 'value' doesn't exist here
}
out
不一定必须初始化参数,因此编译器将不允许您从out
参数中读取内容,除非您已对其进行了写入。
nameOut
您的if
声明,因为之前未分配任何内容。
out
是的更多约束版本ref
。
在方法主体中,您需要out
在离开方法之前分配给所有参数。同样,分配给out
参数的值也将被忽略,而ref
要求分配它们。
因此out
,您可以执行以下操作:
int a, b, c = foo(out a, out b);
在哪里ref
需要分配a和b。
out
则是约束较少的版本。 ref
具有“先决条件:绝对已分配变量,后置条件:绝对已分配变量”,而out
只有“后置条件:绝对已分配变量”。(并且正如预期的那样,具有更少先决条件的函数实现需要更多的资源)
怎么听起来:
出 =仅初始化/填充参数(参数必须是空的)归还了普通
REF =基准,标准参数(可能与值),但功能可以modifiy它。
您可以out
在两个上下文中使用contextual关键字(每个上下文都是详细信息的链接),作为参数修饰符或在接口和委托中的泛型类型参数声明中使用。本主题讨论参数修饰符,但您可以参阅此其他主题,以获取有关泛型类型参数声明的信息。
该out
关键字的原因参数按引用传递。这类似于ref
关键字,不同之处在于它ref
要求在传递变量之前对其进行初始化。要使用out
参数,方法定义和调用方法都必须显式使用out
关键字。例如:C#
class OutExample
{
static void Method(out int i)
{
i = 44;
}
static void Main()
{
int value;
Method(out value);
// value is now 44
}
}
尽管作为out
参数传递的变量不必在传递之前进行初始化,但是在方法返回之前,被调用方法需要分配一个值。
尽管ref
和out
关键字会导致不同的运行时行为,但在编译时它们不被视为方法签名的一部分。因此,如果唯一的区别是一个方法接受一个ref
参数,而另一个方法接受一个参数,则不能重载方法out
。例如,以下代码将无法编译:C#
class CS0663_Example
{
// Compiler error CS0663: "Cannot define overloaded
// methods that differ only on ref and out".
public void SampleMethod(out int i) { }
public void SampleMethod(ref int i) { }
}
但是,如果一个方法采用ref
或out
参数,而另一个方法均不使用,则可以完成重载,例如:C#
class OutOverloadExample
{
public void SampleMethod(int i) { }
public void SampleMethod(out int i) { i = 5; }
}
属性不是变量,因此不能作为out
参数传递。
有关传递数组的信息,请参见《使用ref
和传递数组》out
(《 C#编程指南》)。
您不能将ref
and out
关键字用于以下几种方法:
Async methods, which you define by using the async modifier.
Iterator methods, which include a yield return or yield break statement.
例
out
当您要一个方法返回多个值时,声明一个方法很有用。下面的示例使用out
一个方法调用返回三个变量。请注意,第三个参数分配为null。这使方法可以选择返回值。C#
class OutReturnExample
{
static void Method(out int i, out string s1, out string s2)
{
i = 44;
s1 = "I've been returned";
s2 = null;
}
static void Main()
{
int value;
string str1, str2;
Method(out value, out str1, out str2);
// value is now 44
// str1 is now "I've been returned"
// str2 is (still) null;
}
}
只是为了澄清OP的注释,对ref和out的使用是“对方法外部声明的值类型或结构的引用”,这已经被错误地建立了。
考虑在StringBuilder上使用ref,这是一种引用类型:
private void Nullify(StringBuilder sb, string message)
{
sb.Append(message);
sb = null;
}
// -- snip --
StringBuilder sb = new StringBuilder();
string message = "Hi Guy";
Nullify(sb, message);
System.Console.WriteLine(sb.ToString());
// Output
// Hi Guy
为此:
private void Nullify(ref StringBuilder sb, string message)
{
sb.Append(message);
sb = null;
}
// -- snip --
StringBuilder sb = new StringBuilder();
string message = "Hi Guy";
Nullify(ref sb, message);
System.Console.WriteLine(sb.ToString());
// Output
// NullReferenceException
基本上都ref
和out
传递方法之间对象/值
out关键字使参数通过引用传递。这类似于ref关键字,不同之处在于ref要求在传递变量之前先对其进行初始化。
out
:参数未初始化,必须在方法中初始化
ref
:参数已经初始化,可以在方法中读取和更新。
引用类型的“ ref”有什么用?
您可以将给定的引用更改为其他实例。
你知道吗?
尽管ref和out关键字导致不同的运行时行为,但在编译时它们不被视为方法签名的一部分。因此,如果唯一的区别是一个方法采用ref参数而另一个方法采用out参数,则方法不能重载。
您不能将ref和out关键字用于以下几种方法:
属性不是变量,因此不能作为输出参数传递。
有关C#7的附加说明:
在C#7中,无需使用out预先声明变量。所以这样的代码:
public void PrintCoordinates(Point p)
{
int x, y; // have to "predeclare"
p.GetCoordinates(out x, out y);
WriteLine($"({x}, {y})");
}
可以这样写:
public void PrintCoordinates(Point p)
{
p.GetCoordinates(out int x, out int y);
WriteLine($"({x}, {y})");
}
资料来源: C#7的新功能。
仍然感到需要一个很好的摘要,这是我想出的。
当我们在函数内部时,这就是我们指定变量数据访问控制的方式,
in
= R
out
=必须在R之前W
ref
= R + W
in
函数只能读取该变量。
out
不得先初始化变量,因为
函数必须在READ之前写给它。
ref
函数可以对该变量进行读/写。
着眼于修改数据的地方,
in
只能在输入(输入)功能之前设置数据。
out
只能在退出(退出)功能之前设置数据。
ref
在输入(输入)功能之前必须设置数据。
可以在退出(退出)功能之前设置数据。
请注意,in
从C#ver 7.2开始,它是一个有效的关键字:
in参数修饰符在C#7.2和更高版本中可用。早期版本会生成编译器错误CS8107(“在C#7.0中不提供功能'只读引用'。请使用语言版本7.2或更高版本。”)要配置编译器语言版本,请参阅选择C#语言版本。
...
in关键字使参数通过引用传递。它使形式参数成为参数的别名,该参数必须是变量。换句话说,对参数进行的任何操作都在参数上进行。就像ref或out关键字,不同之处在于in参数不能被调用的方法修改。可以修改ref参数,而out参数必须通过调用的方法进行修改,并且这些修改在调用上下文中是可见的。
out
分配给它之前无法读取。ref
没有这个限制。就是这样。