六、 引用的语义

第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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值