使用 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
局部变量。ref
return。ref
局部变量和ref
return的只读版。in
参数:ref
参数的只读版。- 只读结构体,让
in
参数以及只读ref
局部变量和ref
return可以避免复制。 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)
评论
发表评论