目录
- params
-
- 用法
- ref
-
- 用法
-
- 按引用传递参数
- 引用返回值
- 引用局部变量
- in
-
- 用法
-
- 特殊注意点
- 优势
- out
-
- 用法
- 版本变迁信息
- 其他
-
- ref、in和out三者异同
- 方法重载
C#中ref和out关键字的应用以及区别。
官方文档
编写安全有效的 C# 代码
- params 指定此参数采用可变数量的参数。
- in 指定此参数由引用传递,但只由调用方法读取。
- ref 指定此参数由引用传递,可能由调用方法读取或写入。
- out 指定此参数由引用传递,由调用方法写入。
params
使用 params 关键字可以指定采用数目可变的参数的方法参数。
用法
- 参数类型必须是一维数组。
- 在方法声明中的 params 关键字之后不允许有任何其他参数,并且在方法声明中只允许有一个 params 关键字。
- 果 params 参数的声明类型不是一维数组,则会发生编译器错误 CS0225。
使用 params 参数调用方法时,可以传入:
(1)数组元素类型的参数的逗号分隔列表;
(2)指定类型的参数的数组;
(3)无参数。 如果未发送任何参数,则 params 列表的长度为零。
public class MyClass
{
public static void UseParams(params int[] list)
{
for (int i = 0; i < list.Length; i++) Console.Write(list[i] + " ");
Console.WriteLine();
}
public static void UseParams2(params object[] list)
{
for (int i = 0; i < list.Length; i++) Console.Write(list[i] + " ");
Console.WriteLine();
}
static void Main()
{
UseParams(1, 2, 3, 4);
UseParams2(1, 'a', "test");
UseParams2();
int[] myIntArray = { 5, 6, 7, 8, 9 };
UseParams(myIntArray);
object[] myObjArray = { 2, 'b', "test", "again" };
UseParams2(myObjArray);
UseParams2(myIntArray);
}
}
/*
Output:
1 2 3 4
1 a test
5 6 7 8 9
2 b test again
System.Int32[]
*/
上面的最后一个样例中,myIntArray是int数组,也是一个object对象,所以输出的是System.Int32[],相当于传入参数的长度是1。
至于 UseParams(myObjArray);会引起编译错误,因为object数组无法转换为int数组。
ref
在方法的参数列表中使用 ref 关键字时,它指示参数按引用传递,而非按值传递。 ref 关键字让形参成为实参的别名,这必须是变量。 换而言之,对形参执行的任何操作都是对实参执行的。
ref就是reference(引用)的缩写,关于引用传递和值传递就没有必要说了。
用法
ref 关键字指示按引用传递值。 它用在四种不同的上下文中:
- 在方法签名和方法调用中,按引用将参数传递给方法;
- 在方法签名中,按引用将值返回给调用方;
- 在成员正文中,指示引用返回值是否作为调用方欲修改的引用被存储在本地;
- 在 struct 声明中,声明 ref struct 或 readonly ref struct。
按引用传递参数
按引用传递引用类型使所调用方能够替换调用方中引用参数引用的对象。 对象的存储位置按引用参数的值传递到方法。
方法定义和调用方法均必须显式使用 ref 关键字:
void Method(ref int refArgument)
{
refArgument = refArgument + 44;
}
int number = 1;
Method(ref number);
Console.WriteLine(number); // Output: 45
属性不是变量,是方法,不能传递 ref 参数。
引用返回值
引用返回值(或 ref 返回值)是由方法按引用向调用方返回的值。
即是说,调用方可以修改方法所返回的值,此更改反映在调用方法中的对象的状态中。
这个用法我感觉好诡异啊。。。
public class Book
{
public string Author;
public string Title;
}
public class BookCollection
{
private Book[] books = { new Book { Title = "Call of the Wild, The", Author = "Jack London" },
new Book { Title = "Tale of Two Cities, A", Author = "Charles Dickens" }
};
private Book nobook = null;
public ref Book GetBookByTitle(string title)
{
for (int ctr = 0; ctr < books.Length; ctr++)
{
if (title == books[ctr].Title)
return ref books[ctr];
}
return ref nobook;
}
public void ListBooks()
{
foreach (var book in books)
{
Console.WriteLine($"{book.Title}, by {book.Author}");
}
Console.WriteLine();
}
}
调用上面的方法
var bc = new BookCollection();
bc.ListBooks();
ref var book = ref bc.GetBookByTitle("Call of the Wild, The");
if (book != null)
book = new Book { Title = "Republic, The", Author = "Plato" };
bc.ListBooks();
// The example displays the following output:
// Call of the Wild, The, by Jack London
// Tale of Two Cities, A, by Charles Dickens
//
// Republic, The, by Plato
// Tale of Two Cities, A, by Charles Dickens
因为使用了ref,所以这里的book就是books[0],就是bc.books[0],后面给book重新赋值了,bc.books[0]也就改变了值。
如果以下两个条件都成立,请考虑使用 ref readonly return返回:通过返回 ref readonly 可以保存复制较大的结构并保留内部数据成员的不变性。
- 返回值是大于 IntPtr.Size 的 struct。
- 存储生存期大于返回值的方法。
引用局部变量
ref 局部变量用于指代使用 return ref 返回的值。 无法将 ref 局部变量初始化为非 ref 返回值。 在某些情况下,按引用访问值可避免潜在的高开销复制操作,从而提高性能。
in
in 关键字会导致按引用传递参数,但确保未修改参数。
添加 in 修饰符,按引用传递参数,并声明设计意图是为了按引用传递参数,避免不必要的复制操作。 你不打算修改用作该参数的对象。
用法
in 参数无法通过调用的方法进行修改。
int readonlyArgument = 44;
InArgExample(readonlyArgument);
Console.WriteLine(readonlyArgument); // value is still 44
void InArgExample(in int number)
{
// Uncomment the following line to see error CS8331
//number = 19;
}
由于编译器可为任何 in 参数创建临时变量,因此还可指定任何 in 参数的默认值。 以下代码指定原点(点 0,0,0)为第二个点的默认值:
private static double CalculateDistance2(in Point3D point1, in Point3D point2 = default)
{
double xDifference = point1.X - point2.X;
double yDifference = point1.Y - point2.Y;
double zDifference = point1.Z - point2.Z;
return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}
特殊注意点
有以下方法:
static void Method(in int argument)
{
// implementation removed
}
- 临时变量允许将编译时常数作为 in 参数。
Method(5); // OK, temporary variable created.
Method(5L); // CS1503: no implicit conversion from long to int
- 存在从实参类型到形参类型的隐式转换时,临时变量允许使用实参。
short s = 0;
Method(s); // OK, temporary int created with the value 0
// 直接传 s ,会新生成一个临时的 int 变量
Method(in s); // CS1503: cannot convert from in short to in int
// 这里 in s 的话会把s的引用地址传过去,显然不能往int上套
- 临时变量允许使用属性或 in 参数的其他表达式。(没有看明白。。。)
- in关键字可以隐式使用。
int i = 42;
Method(i); // passed by readonly reference
Method(in i); // passed by readonly reference, explicitly using `in`
如果存在方法重载,in就不能隐式使用了
static void Method(int argument)
{
// implementation removed
}
static void Method(in int argument)
{
// implementation removed
}
Method(5); // Calls overload passed by value
Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // Calls overload passed by value.
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // Calls overload passed by value
Method(in i); // passed by readonly reference, explicitly using `in`
优势
定义使用 in 参数的方法是一项潜在的性能优化。
某些 struct 类型参数可能很大,在紧凑的循环或关键代码路径中调用方法时,复制这些结构的成本就很高。
方法声明 in 参数以指定参数可能按引用安全传递,因为所调用的方法不修改该参数的状态, 按引用传递这些参数可以避免(可能产生的)高昂的复制成本。

【注】为了简化操作,前面的代码将 int 用作参数类型。 因为大多数新式计算机中的引用都比 int 大,所以将单个 int 作为只读引用传递没有任何好处。
out
与 ref 关键字相似,只不过 ref 要求在传递之前初始化变量;
也类似于 in 关键字,只不过 in 不允许通过调用方法来修改参数值。
用法
不需要初始化,但是在方法返回前必须赋值,方法定义和调用方法均必须显式使用 out 关键字。
int initializeInMethod;
OutArgExample(out initializeInMethod);
Console.WriteLine(initializeInMethod); // value is now 44
void OutArgExample(out int number)
{
number = 44; // 这句话必须存在,否则报错
}
下面这样也不行
out 关键字也可与泛型类型参数结合使用,以指定该类型参数是协变参数。
属性不是变量,因此不能作为 out 参数传递。
版本变迁信息
在 C# 6 及更早版本中,必须先在单独的语句中声明变量,然后才能将其作为 out 参数传递。
string numberAsString = "1640";
int number;
if (Int32.TryParse(numberAsString, out number))
Console.WriteLine($"Converted '{numberAsString}' to {number}");
else
Console.WriteLine($"Unable to convert '{numberAsString}'");
// The example displays the following output:
// Converted '1640' to 1640
从 C# 7.0 开始,可以在方法调用的参数列表而不是单独的变量声明中声明 out 变量。
string numberAsString = "1640";
if (Int32.TryParse(numberAsString, out int number))
Console.WriteLine($"Converted '{numberAsString}' to {number}");
else
Console.WriteLine($"Unable to convert '{numberAsString}'");
// The example displays the following output:
// Converted '1640' to 1640
其他
ref、in和out三者异同
- 传递到 ref 或 in 形参的实参必须先经过初始化,然后才能传递。 out 形参在传递之前,不需要显式初始化该形参的实参。
- in 参数无法通过调用的方法进行修改;out 参数必须由调用的方法进行修改,这些修改在调用上下文中是可观察的;而 ref 参数是可以修改的。
- 在其他要求签名匹配的情况下(如隐藏或重写),in、ref 和 out 是签名的一部分,相互之间不匹配。
-
不能将 ref、in 和 out 关键字用于以下几种方法:
(1)异步方法,通过使用 async 修饰符定义;
(2)迭代器方法,包括 yield return 或 yield break 语句。
-
扩展方法还限制了对以下这些关键字的使用:
(1)不能对扩展方法的第一个参数使用 out 关键字;
(2)当参数不是结构或是不被约束为结构的泛型类型时,不能对扩展方法的第一个参数使用 ref 关键字;
(3)除非第一个参数是结构,否则不能使用 in 关键字。 即使约束为结构,也不能对任何泛型类型使用 in 关键字。
方法重载
类的成员不能具有仅在 ref、in 或 out 方面不同的签名。 如果类型的两个成员之间的唯一区别在于其中一个具有 ref 参数,而另一个具有 out 或 in 参数,则会发生编译器错误。
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、in 或 out 参数,另一个方法具有值传递的参数时,则可以重载方法,如下面的示例所示。
class RefOverloadExample
{
public void SampleMethod(int i) { }
public void SampleMethod(ref int i) { }
}