使用 ref 参数或者in参数的扩展方法(C# 7.2)
在C# 7.2之前,任何扩展方法的第一个参数都必须是值参数。C# 7.2取消了这项限制,于是ref相关语义应用得更彻底了。
一、 在扩展方法中使用ref/in参数来规避复制
假设有一个大型结构体,我们想避免复制它。另外,有一个方法根据该结构体的几个属性值计算一个三维向量坐标。如果该结构体自带这样的方法(或者属性),自然可以规避复制过程;若是该结构体声明为只读,毫无疑问可以规避复制。若想实现结构体作者未曾考虑过的复杂操作,该怎么办呢?代码清单13-17提供了一个只读的Vector3D结构体,该结构体只有3个属性X、Y、Z。
代码清单13-17
Vector3D结构体的小例子
public readonly struct Vector3D
{
public double X { get; }
public double Y { get; }
public double Z { get; }
public Vector3D(double x, double y, double z)
{
X = x;
Y = y;
Z = z;
}
}可以自己编写一个接收in结构体参数的方法,这样做虽然能够避免复制,但调用时略显奇怪,最后写出的调用代码可能如下所示:
double magnitude = VectorUtilities.Magnitude(vector);这种写法不太优雅。如果使用扩展方法,则每次调用时都要复制该结构体:
public static double Magnitude(this Vector3D vector)在可读性和性能之间进行取舍令人苦恼。C# 7.2提出了一种合理的改进方式:编写扩展方法时,第一个参数前可以添加ref或者in修饰符。修饰符可以位于this前,也可以位于this后。如果只需要计算出一个新值,那么可以使用in修饰符;如果需要修改原始内存位置上的值,又不想创建并复制一个新值,可以选用ref修饰符。代码清单13-18中包含了对于Vector3D的两种扩展方法。
代码清单13-18 使用
ref和in修饰符的扩展方法
public static double Magnitude(this in Vector3D vec) =>
Math.Sqrt(vec.X * vec.X + vec.Y * vec.Y + vec.Z * vec.Z);
public static void OffsetBy(this ref Vector3D orig, in Vector3D off) =>
orig = new Vector3D(orig.X + off.X, orig.Y + off.Y, orig.Z + off.Z);我通常不会给参数取这种简短的名称,但是由于书页排版的原因不得不将参数名简化。OffsetBy方法的第2个参数也添加了in修饰符,因为我们想尽量避免复制操作。
扩展方法易于使用。唯一需要注意的是,和普通ref参数不同,在调用扩展方法时不需要指定ref修饰符。代码清单13-19调用了前面的两个扩展方法来创建两个向量,使用第2个向量为第1个向量增加偏移量,然后打印结果向量及其大小。
代码清单13-19 调用
ref参数和in参数的扩展方法
var vector = new Vector3D(1.5, 2.0, 3.0);
var offset = new Vector3D(5.0, 2.5, -1.0);
vector.OffsetBy(offset);
Console.WriteLine($"({vector.X}, {vector.Y}, {vector.Z})");
Console.WriteLine(vector.Magnitude());执行结果如下:
(6.5, 4.5, 2)
8.15475321515004调用OffsetBy方法修改vector变量的目的达成。
说明
OffsetBy方法似乎让不可变的Vector3D结构体可变了。该特性只是初出茅庐,还有许多地方需要提升。就目前而言,我个人更愿意编写in参数的扩展方法。
带有in参数的扩展方法,可以在读写属性的变量上调用(例如vector.Magnitude()),而带有ref参数的扩展方法无法在只读变量上调用。如果为vector创建一个只读别名,则无法调用OffsetBy方法:
ref readonly var alias = ref vector;
alias.OffsetBy(offset); <------ 非法:将只读变量用作ref变量与普通扩展方法不同,ref和in参数的扩展方法的目标类型(第一个参数的类型)是存在限制的。
二、 ref和in扩展方法的使用限制
普通的扩展方法可以针对任何类型进行扩展。扩展方法使用的类型可以是普通类型,也可以是有类型约束或者无类型约束的类型形参:
static void Method(this string target)
static void Method(this IDisposable target)
static void Method<T>(this T target)
static void Method<T>(this T target) where T : IComparable<T>
static void Method<T>(this T target) where T : struct而ref和in扩展方法只能扩展值类型。在in扩展方法中,该值类型也不能是类型形参。以下声明合法:
static void Method(this ref int target)
static void Method<T>(this ref T target) where T : struct
static void Method<T>(this ref T target) where T : struct, IComparable<T>
static void Method<T>(this ref int target, T other)
static void Method(this in int target)
static void Method(this in Guid target)
static void Method<T>(this in Guid target, T other)而以下声明非法:
static void Method(this ref string target) <------ 引用类型target用于ref参数
static void Method<T>(this ref T target) (本行及以下1行) 类型形参target用于ref参数,但是缺少struct类型约束
where T : IComparable<T>
static void Method<T>(this in string target) <------ 引用类型target用于in参数
static void Method<T>(this in T target) (本行及以下1行) 类型形参target用于in参数
where T : struct需要注意in和ref的区别:ref参数可以是类型形参,只要它具备一个struct的类型约束;in扩展方法可以是泛型的(参见合法示例的最后一个),但被扩展的类型不能是类型形参。目前还没有类型约束能够规定T是一个readonly struct。在将来的C#版本中这一点可能会发生变化。
扩展类型必须是值类型,这主要有以下两个原因。
- 该特性就是用于避免复制值所导致的性能消耗的,而引用类型不存在这样的性能消耗。
- 如果
ref参数是引用类型,那么它可能是null引用。这样就违背了目前C#开发人员和工具的一条假定:x.Method()(x如果是一个引用类型变量)的调用中,x不能为null。
ref和in扩展方法的应用不会特别广泛,但是它们的出现确实增强了C#语言的内在一致性。
本章内容概览中提到的特性和目前介绍的特性有些出入,回顾如下。
ref局部变量。refreturn。ref局部变量和refreturn的只读版。in参数:ref参数的只读版。- 只读结构体,让
in参数以及只读ref局部变量和refreturn可以避免复制。 ref和in参数的扩展方法。
如果从ref参数出发,思考应该如何扩展这个概念,就可能得出一个类似的特性清单。接下来介绍类ref结构体的相关内容,虽然该特性和前面介绍的特性有一定相关性,但像是全新的类型。
(完)
相关阅读:
C# 回顾:ref 知多少
C# ref 局部变量和 ref return
in 参数(C# 7.2)
将结构体声明为只读(C# 7.2)
使用 ref 参数或者in参数的扩展方法(C# 7.2)
类 ref 结构体(C# 7.2)
评论
发表评论