什么时候使用in、ref、out

前几天有人问我什么时候应该使用参数关键字out而不是ref。虽然我(我认为)理解refout关键字之间的区别(已经是问之前)和最好的解释似乎是ref == inout,有什么(假设或代码)的例子,我应该总是使用out而不是ref

既然ref更通用,为什么要使用out呢?它只是语法上的糖吗?

202558 次浏览

使用out表示该参数未被使用,仅被设置。这有助于调用者理解您总是在初始化参数。

此外,ref和out不仅适用于值类型。它们还允许您重置引用类型从方法中引用的对象。

out是更加约束的ref版本。

在方法体中,你需要在离开方法之前给所有out参数赋值。 另外,赋值给out参数的值也会被忽略,而ref要求赋值

所以out允许你做:

int a, b, c = foo(out a, out b);

其中ref需要赋值a和b。

你应该使用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同时提供“in”和“out”功能,而out只提供“out”功能。有一些事情需要考虑:

  1. out要求方法接受参数MUST,在返回之前的某个时刻,将值赋给变量。你可以在一些键/值数据存储类中找到这种模式,比如Dictionary<K,V>,其中有TryGetValue这样的函数。此函数接受一个out形参,该形参保存检索到的值。调用者将值传递给这个函数是没有意义的,因此out用于保证调用后变量中会有一些值,即使它不是“真实”数据(在TryGetValue的情况下,键不存在)。
  2. 在处理互操作代码时,outref参数的封送方式不同

另外,作为题外话,重要的是要注意,虽然引用类型和值类型在其值的性质上有所不同,应用程序中的每个变量都指向存储值的内存位置,即使对于引用类型也是如此。对于引用类型,该内存位置中包含的值恰好是另一个内存位置。当您将值传递给一个函数(或执行任何其他变量赋值)时,该变量的值将复制到另一个变量中。对于值类型,这意味着复制类型的整个内容。对于引用类型,这意味着复制内存位置。无论采用哪种方法,它都会创建变量中包含的数据的副本。唯一真正相关的是赋值语义;当给一个变量赋值或按值传递(默认值)时,当对原始(或新)变量进行新赋值时,它不会影响其他变量。在引用类型的情况下,对实例所做的更改在两边都可用,但这是因为实际的变量只是指向另一个内存位置的指针;变量的内容——内存位置——实际上并没有改变。

传递ref关键字表示原始变量而且和函数参数实际上指向相同的内存位置。同样,这只影响赋值语义。如果将一个新值赋给其中一个变量,那么由于其他变量指向相同的内存位置,新值将反映在另一侧。

只是在OP的注释中澄清一下,对ref和out的使用是“对在方法外部声明的值类型或结构的引用”,这已经在incorrect中建立了。

考虑在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

它取决于编译上下文(参见下面的示例)。

outref都表示通过引用传递变量,但ref要求变量在传递之前进行初始化,这在封送处理上下文中可能是一个重要的区别(互操作:UmanagedToManagedTransition或反之亦然)。

MSDN警告:

不要将按引用传递的概念与引用类型的概念混淆。这两个概念是不一样的。不管方法形参是值类型还是引用类型,都可以通过ref来修改。通过引用传递值类型时,没有对值类型进行装箱。

来自MSDN官方文档:

out关键字使参数通过引用传递。这类似于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中,myfuncOutmyfuncRef的指令与预期相同。

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指令清单)

如果计划读写参数,则需要使用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返回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
}

作为ref传递的参数必须在传递给方法之前初始化,而out形参在传递给方法之前不需要初始化。

下面是一些注释,我从这篇codeproject文章c# Out Vs Ref

  1. 只有当我们期望从一个函数或方法得到多个输出时,才应该使用它。对结构的思考也是一个不错的选择。
  2. REF和OUT是关键字,它们指示数据如何从调用方传递到被调用方,反之亦然。
  3. 在REF中,数据以两种方式传递。从呼叫者到被呼叫者,反之亦然。
  4. In Out数据仅以一种方式从被调用方传递给调用方。在这种情况下,如果Caller试图向被调用方发送数据,它将被忽略/拒绝。

如果你是一个视觉人,那么请看看你的视频,它展示了两者的区别

下图更直观地显示了差异

 c# Out Vs Ref

你可以在两个上下文中使用out上下文关键字(每个都是到详细信息的链接),作为参数修饰符或在接口和委托中的泛型类型参数声明中使用。本主题讨论参数修饰符,但有关泛型类型参数声明的信息,请参阅另一个主题。

out关键字使参数通过引用传递。这类似于ref关键字,除了ref要求在传递变量之前对其进行初始化。要使用out形参,方法定义和调用方法都必须显式使用out关键字。例如: c# < / p >
class OutExample
{
static void Method(out int i)
{
i = 44;
}
static void Main()
{
int value;
Method(out value);
// value is now 44
}
}

虽然作为out参数传递的变量在传递之前不必初始化,但被调用的方法需要在方法返回之前赋值。

虽然refout关键字导致不同的运行时行为,但它们在编译时不被认为是方法签名的一部分。因此,如果唯一的区别是一个方法接受ref参数,另一个方法接受out参数,则方法不能重载。例如,下面的代码将无法编译: c# < / p >
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) { }
}
但是,如果一个方法使用refout参数,而另一个方法两者都不使用,则可以进行重载,如下所示: c# < / p >
class OutOverloadExample
{
public void SampleMethod(int i) { }
public void SampleMethod(out int i) { i = 5; }
}

属性不是变量,因此不能作为out参数传递。

有关传递数组的信息,请参见使用refout传递数组(c#编程指南)。

以下类型的方法不能使用refout关键字:

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# < / p >
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;
}
}

< >强读音:

=只初始化/填充一个参数(参数必须为空)将其返回为 plain

Ref =引用,标准参数(可能带有值),但函数可以修改它。

你为什么要用out?

让其他人知道当变量从被调用的方法返回时将被初始化!

如上所述: "用于out参数调用方法需要在方法返回之前赋值。"< / p >

例子:

Car car;
SetUpCar(out car);
car.drive();  // You know car is initialized.

基本上是refout用于在方法之间传递对象/值

out关键字使参数通过引用传递。这类似于ref关键字,只不过ref要求在传递变量之前对其进行初始化。

out:参数没有初始化,必须在方法中初始化

ref:参数已经初始化,可以在方法中读取和更新。

引用类型的“ref”有什么用?

您可以将给定的引用更改为不同的实例。

你知道吗?

  1. 尽管ref和out关键字会导致不同的运行时行为,但它们在编译时不被视为方法签名的一部分。因此,如果唯一的区别是一个方法使用ref参数,而另一个方法使用out参数,则方法不能重载。

  2. 以下类型的方法不能使用ref和out关键字:

    • Async方法,通过使用Async修饰符来定义。
    • 迭代器方法,其中包括yield return或yield break语句。
    • 李< / ul > < / >
    • 属性不是变量,因此不能作为参数传递。

关于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的新特性是什么?

如何在c#中使用inoutref ?

  • C#中的所有关键字都具有相同的功能,但包含一些边界
  • in参数不能被调用的方法修改。
  • ref参数可以被修改。
  • ref在被调用者使用之前必须初始化,它可以在方法中读取和更新。
  • out参数必须由调用者修改。
  • out参数必须在方法中初始化
  • 变量通过了作为in参数必须在方法调用中传递之前初始化。然而,被调用的方法不能赋值或修改实参。

以下类型的方法不能使用inrefout关键字:

  • 异步方法,可以使用async修饰符来定义。
  • 迭代器方法,其中包含yield returnyield break语句。

应该注意的是,inc# 7.2之前是一个有效的关键字:

in参数修饰符在c# 7.2及更高版本中可用。以前的版本会生成编译器错误CS8107(“只读引用”特性在c# 7.0中不可用。请使用7.2或更高版本的语言。若要配置编译器语言版本,请参见选择c#语言版本。

...

in关键字使参数通过引用传递。它使形式形参成为实参的别名,实参必须是变量。换句话说,对形参的任何操作都是对实参进行的。它类似于ref或out关键字,只不过in参数不能被调用的方法修改。ref参数可以修改,而out参数必须由被调用的方法修改,并且这些修改在调用上下文中是可观察到的。

还是觉得需要好好总结一下,这就是我想出来的。

总结,

当我们在函数中,这就是我们如何指定变量数据访问控制,

__abc0 = r

out =必须W在R之前

__abc0 = r + w


解释,

in

函数只能那个变量。

out

变量不能先初始化,因为,

. function 必须写之前

ref

函数可以读/写赋值给该变量。


为什么要这样命名呢?

关注数据被修改的地方,

in

数据必须在输入(in)函数之前设置。

out

数据必须只在离开(out)函数之前设置。

ref

输入(in)函数前必须设置的数据 在离开(out)函数之前可以设置数据。< / p >