使用 ref 参数或者in参数的扩展方法(C# 7.2)

在C# 7.2之前,任何扩展方法的第一个参数都必须是值参数。C# 7.2取消了这项限制,于是ref相关语义应用得更彻底了。

假设有一个大型结构体,我们想避免复制它。另外,有一个方法根据该结构体的几个属性值计算一个三维向量坐标。如果该结构体自带这样的方法(或者属性),自然可以规避复制过程;若是该结构体声明为只读,毫无疑问可以规避复制。若想实现结构体作者未曾考虑过的复杂操作,该怎么办呢?代码清单13-17提供了一个只读的Vector3D结构体,该结构体只有3个属性XYZ

代码清单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 使用refin修饰符的扩展方法

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变量

与普通扩展方法不同,refin参数的扩展方法的目标类型(第一个参数的类型)是存在限制的。

普通的扩展方法可以针对任何类型进行扩展。扩展方法使用的类型可以是普通类型,也可以是有类型约束或者无类型约束的类型形参:

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

refin扩展方法只能扩展值类型。在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

需要注意inref的区别:ref参数可以是类型形参,只要它具备一个struct的类型约束;in扩展方法可以是泛型的(参见合法示例的最后一个),但被扩展的类型不能是类型形参。目前还没有类型约束能够规定T是一个readonly struct。在将来的C#版本中这一点可能会发生变化。

扩展类型必须是值类型,这主要有以下两个原因。

  • 该特性就是用于避免复制值所导致的性能消耗的,而引用类型不存在这样的性能消耗。
  • 如果ref参数是引用类型,那么它可能是null引用。这样就违背了目前C#开发人员和工具的一条假定:x.Method()x如果是一个引用类型变量)的调用中,x不能为null

refin扩展方法的应用不会特别广泛,但是它们的出现确实增强了C#语言的内在一致性。

本章内容概览中提到的特性和目前介绍的特性有些出入,回顾如下。

  • ref局部变量。
  • ref return。
  • ref局部变量和ref return的只读版。
  • in参数:ref参数的只读版。
  • 只读结构体,让in参数以及只读ref局部变量和ref return可以避免复制。
  • refin参数的扩展方法。

如果从ref参数出发,思考应该如何扩展这个概念,就可能得出一个类似的特性清单。接下来介绍类ref结构体的相关内容,虽然该特性和前面介绍的特性有一定相关性,但像是全新的类型。

(完)

相关阅读:


C# 回顾:ref 知多少

C# ref 局部变量和 ref return

in 参数(C# 7.2)

将结构体声明为只读(C# 7.2)

使用 ref 参数或者in参数的扩展方法(C# 7.2)

类 ref 结构体(C# 7.2)

评论

此博客中的热门博文

in 参数(C# 7.2)

C# ref 局部变量和 ref return

类 ref 结构体(C# 7.2)