C#7.0 ref引用传递

2022-12-07 20:57:41 浏览数 (1)

1.概要

在工作中大家用到引用类型是非常多的,大家都知道引用类型在使用过程中传递的是对象引用并不会发生整个对象复制。而值类型在传递的过程中就不一样了,我曾经在编写代码时希望通过值类型来压低应用程序的内存占用,在高并发的情况大量的对象需要在程序里流转这个时候看内存监测的时候会发现,内存并没有变少。虽说值类型能提供很多好处但有一个缺点就是会发生复制,那么如何规避复制这个缺点呢?我们往下看(本文只是简略的分享,实际上要把这一整块讲明白几千字是远远不够的)。

2.详细内容

2.1 ref

在继续了解C#7.0的ref特性需要了解一些前置知识点,首先是变量和值的区别。

(下图)变量包含内容:

  • 变量名称
  • 编译时类型
  • 当前值

变量的声明本质是在内存中开辟一段内存空间,给变量x赋值相当于是覆盖了之前的值。当变量类型是引用类型时,控件里的值不再是对象本身。而是对象的引用,就是通过内存地址找到对象。(如果加上ref关键字,ref的引用和对象引用是不同的概念。通过值传递对象引用和通过引用传递变量是不同的。)

当把某个变量值复制给另外一个变量时,只是这个值本身发生了复制。这两个变量依然是独立的,之后任何一个变量的值修改不会影响另外一个变量。

这种方式的值复制,和调用方法时对值参数的操作是相同的:方法实参的值被复制到了另一个新的空间中。

而ref参数的行为与此不同。使用ref参数,不会创建开辟新的空间,而是调用放提供一个现有的包含初始值的空间。可以理解为一个空间同时被两个地址指向:一个是调用方使用的该变量的表示,另一个是形参的名称。

如果在方法中修改了ref参数的值,即修改了纸上的现有值。当方法返回时,修改的结果就会反回给调用方,因为修改的是同一个命名空间的值。

2.2 ref readonly

前面提到的变量都是可写变量,以下两个独立场景中,只允许ref可写变量就显得有些不足了。

  • 可能需要给某个只读字段添加引用地址,避免复制以提升效率。
  • 可能需要只允许通过ref变量进行只读访问。

C#7.2加入了ref readonly解决了上述问题。ref局部变量可以使用readonly进行修饰,得到的结果自然是只读的,就像只读字段一样。不能为只读变量赋新值,如果它是结构体,则不能修改任何字段或者调用属性的setter方法。

代码示例:

代码语言:javascript复制
    public readonly struct Juster
    {
        public int Age { get; }

        public Juster(int age) => Age = age;
    }

使用readonly的两处都需要协作:如果调用一个带有ref readonly返回的方法或者索引器,并且需要将结果保存到一个局部变量中,那么这个局部变量必须由ref readonly修饰。

代码语言:javascript复制
    internal class Class1
    {
        static readonly int field = DateTime.UtcNow.Second;
        static ref readonly int GetJusterTime() => ref field;

        public void Test() 
        {
            ref readonly int local = ref GetJusterTime();
            Console.WriteLine(local);
        }
    }

2.3 in参数

C#7.2为方法参数加入了in修饰符,该修饰符的使用方式与ref、out相同,但目的不同。一个带有in修饰符参数,可以通过引用传递避免复制提升效率,同时可以保证参数值不被修改。在方法内部,in参数的行为类似于ref readonly局部变量。该变量依然是由调用方传入的一个内存地址,因此要保证方法不会被修改值,否则修改结果回影响调用方,这样就违背了in参数的意义。

安全的使用使用in参数:

代码语言:javascript复制
        public static double PublicMethod(Juster juster) 
        {
            double result = GetScale(in juster);
            return result   result;
        }

        private static double GetScale(in Juster input) => input.Age * input.Age;

这种方式可以防止参数被意外修改,因为方法是私有的,我们可以检查所有调用方,确定它们不会传递哪些在方法执行时可能被修改的参数。在方法GetScale被调用时,每个结构体只会被复制一次,复制之后私有方法调用时都是别名。这样就把自己的代码和其他线程中调用方的任何修改,或者其他方法的副作用隔离开来了。

使用建议:

  • 只有确定性能提升客观,采用in参数,例如使用大型结构体。
  • 在公共api中尽量避免使用in参数,除非即便参数值发生变化,方法也能正确执行。
  • 可以考虑通过公共方法作为防止参数被修改的外部屏障,然后再内部私有方法中使用in参数来减少复制。
  • 对于采用in参数的方法,在调用时考虑显式给出in修饰符。

性能相关测试

https://cloud.tencent.com/developer/article/1402446

https://www.cnblogs.com/BeanHsiang/p/8687780.html

0 人点赞