第3章展示了在将参数传递给方法时所使用的ref关键字。当通过值传递结构时,将复制结构的内容。通过引用传递结构(使用ref关键字),新变量会引用相同的数据。
通过C#7.0,还可以使用ref关键字作为返回类型的修饰符,并作为本地变量的修饰符。在C#7.2中,可以将readonly修饰符添加到ref关键字,以不允许更改。C#7.2还添加了in关键字,以通过引用传递值类型,而不允许他们发生更改。本节将讨论这些新特性。
一方面,最好有不可变类型,因为这些类型允许从多个线程中访问,而不需要同步,因为没有线程可以更改值。然而,不可变类型也意味着需要复制大量数据。对于值类型,需要复制数据,当然,这也会降低性能。使用引用类型,需要不同的变量来引用堆上的相同数据,而且这些数据可能也需要一个副本。例如,string类型是不可变的。像ToUpper和ToLower这样string类型的方法永远不会更改字符串,而是返回一个新字符串。当这些对象不再被引用时,需要收集垃圾。为了避免过度使用垃圾收集器和复制数据,而不需要使用IntPtr和不安全代码,ref关键字的增强功能提供了极大的帮助。
ReferenceSemantics示例使用了如下名称空间:
System
System.Linq
看看下面的Data类。该类包含变量名称为_anumber的值类型int,它在构造函数中初始化。方法Show将数字的当前值写入控制台。最有趣的部分是GetNumber方法。在实现代码中,变量_anumber使用ref关键字返回,以返回对它的引用。这是由GetNumber的返回类型的声明实现的;它是ref int类型的声明,返回一个对int的引用。GetReadonlyNumber方法是一个ref readonly int返回的方法。ref readonly是C#7.2中新增的,通过引用返回值类型,但是不允许由调用者改变:
public class Data
{
public Data(int anumber)=>_anumber = anumber;
private int _anumber;
public ref int GetNumber()=>ref _anumber;
public ref readonly int GetReadonlyNumber()=>ref _anumber;
public void Show()=>System.Console.WriteLine($"Data: {_anumber}");
}
下面使用Data类并调用GetNumber方法。该方法声明为返回ref int,但是在下面的代码片段中,结果写到一个int中。n是一个本地变量,它保存了一个int,GetNumber的结果会复制到这个变量中。更改本地变量的值时,Data类内的数据不会更改:
static void UseMember()
{
System.Console.WriteLine(nameof(UseMember));
var d = new Data(1);
int n = d.GetNumber();
n = 42;
d.Show();
}
运行应用程序时,输出显示,Data类在本地变量更改之后仍然包含初始化的数据:
UseMember
Data: 1
在方法UseReferenceMember的实现中做一个小小的更改,调用GetNumber方法,返回一个ref,它在方法之前指定ref关键字,变量n指定为ref local,因此它在Data类中直接引用_anumber。也可以用ref readonly 修饰符声明本地变量。方法GetNumber返回ref int的结果可以分配给ref readonly int,这保证不能更改变量n2。编译器会抱怨n2是否会被更改:
static void UseMember()
{
System.Console.WriteLine(nameof(UseMember));
var d = new Data(1);
int n = d.GetNumber();
n = 42;
d.Show();
ref readonly int n2 = ref d.GetNumber();
//or
ref readonly int n3 = ref d.GetReadonlyNumber();
//n2 = 42;//not allowed - it's readonly!
}
使用此更改运行应用程序时,Data类中的数据将更改。不需要使用IntPtr和不安全的代码,也可以快速直接地访问:
UseReferenceMember
Data: 42
接下来,调用方法GetReadonlynumber。这个方法返回ref readonly int。可以将结果赋给一个int。把ref赋予int会建立一个副本,副本可以更改,但不会更改原始副本。将结果分配给ref readonly int,会通过引用传递结果,但结果不能更改:
static void UseReadonlyRefMember()
{
System.Console.WriteLine(nameof(UseReadonlyRefMember));
var d = new Data(1);
int n = d.GetReadonlyNumber();//create a copy
n = 42;
d.Show();
//ref int n = d.GetReadonlyNumber();//not allowed
ref readonly int n2 = ref d.GetReadonlyNumber();
//n2 = 42;//not allowed
}
该方法的结果是一个不变的Data成员:
UseReadonlyRefMember
Data: 1
1. 传递ref和返回ref
下面是另一个例子:传递一个ref int并返回一个ref int。Max方法通过ref接收x和y参数,并通过ref返回这两个值的较高者:
static ref int Max(ref int x,ref int y)
{
if(x>y) return ref x;
else return ref y;
}
如果不需要复制变量x和y,将它们传递给方法Max,则可以快速返回较高的值。如果经常调用此方法,这将非常有用:
static void UseMax()
{
System.Console.WriteLine(nameof(UseMax));
int x = 4;
int y = 5;
ref int z = ref Max(ref x,ref y);
System.Console.WriteLine($"{z} is the max of {x} and {y}");
}
返回的消息如下:
UseMax
5 is the max of 4 and 5
返回引用是很快速的,因为幕后只使用指针。但是,这也意味着可以更改引用指向的原始项。例如,改变引用x和y中数据的变量z,根据较大的值,也改变了原始变量的值:
static void UseMax()
{
System.Console.WriteLine(nameof(UseMax));
int x = 4;
int y = 5;
ref int z = ref Max(ref x,ref y);
System.Console.WriteLine($"{z} is the max of {x} and {y}");
z = x+y;
System.Console.WriteLine($"y after changing z: {y}");
}
运行这个程序时,可以看到y现在有了被分配的值。
UseMax
5 is the max of 4 and 5
y after changing z: 9
2. ref和数组
另一个展示ref return和ref local特性的示例使用了该关键字和数组。类Container定义了int[]类型的成员(它们在构造函数中初始化)。GetItem方法通过引用返回数组的一个项。这允许在容器数组中直接使用快速路径:
class Container
{
public Container(int[] data)=>_data = data;
private int[] _data;
public ref int GetItem(int index)=>ref _data[index];
public void ShowAll()
{
System.Console.WriteLine(string.Join(", ",_data));
}
}
当使用这个Container时,一个包含10项列表的样本数组被传递给构造函数。第4项从GetIitem方法中检索,此项更改为33,最后所有项都使用ShowAll方法写入控制台:
static void UseItemOfContainer()
{
System.Console.WriteLine(nameof(UseItemOfContainer));
var c = new Container(Enumerable.Range(0,10).Select(x=>x).ToArray());
ref int item = ref c.GetItem(3);
item = 33;
c.ShowAll();
}
运行应用程序时,可以看到第4项从外部更改:
UseItemOfContainer
0, 1, 2, 33, 4, 5, 6, 7, 8, 9
下面看看添加GetData方法除了处理数组项之外,还可以处理完整的数组。该方法返回一个对数组本身的引用:、
class Container
{
public Container(int[] data)=>_data = data;
private int[] _data;
public ref int GetItem(int index)=>ref _data[index];
public ref int[] GetData()=>ref _data;
public void ShowAll()
{
System.Console.WriteLine(string.Join(", ",_data));
}
}
使用Container类的GetData方法,将返回数组的引用,并将其写入ref本地变量d1中。一个包含三个元素的新数组被分配给这个变量:
static void UseArrayOfContainer()
{
System.Console.WriteLine(nameof(UseArrayOfContainer));
var c = new Container(Enumerable.Range(0,10).Select(p=>p).ToArray());
ref int[] d1 = ref c.GetData();
d1 = new int[]{4,5,6};
c.ShowAll();
}
因为返回对数组的引用,所以可以替换完整的数组。容器现在包含新创建的数组,其中包含元素4、5和6;
UseArrayOfContainer
4, 5, 6
注意:
用于ref returns和ref locals的ref关键字需要在返回引用时保持活跃。例如,只要在引用类型中了包含值类型,就可以返回对值类型的引用,这样它们就在托管的堆中。使用结构,不能定义方法来返回结构成员的引用,可以将对结构的引用返回为引用,如Max方法所示。这些值类型在方法返回时应保证是活跃的,因为它们是由等待方法返回的调用者传递的。
注意:
第3章介绍了使用ref、out和int修饰符定义参数。这些修饰符在引用语义方面也很重要。使用C#7.2的新参数in和值类型,指定值类型是通过引用传递的(类似于通过参数使用ref关键字),但是不允许更改它。对于参数,in类似于ref readonly。