iCsharp in & out & ref

参数传递

Out参数

只用于输出结果

  • 调用前不需要初始化
  • 方法内部必须赋值
  • 引用传递

In参数

引用传递+只读。主要用途是传参时避免大型struct拷贝,并安全的只读访问

  • 调用前必须初始化
  • 方法内部只读
  • 引用传递
  • 传参时可以省略,编译器自动加,但是不建议省略

⭐隐式拷贝⭐

如果结构体不是readonly的,在通过in传递到只读上下文时,如果调用了该函数体内部的某个方法Change,而这个方法会修改结构体内部数据,此时Change会通过拷贝一份该函数体再进行调用,Change里任何对原结构体的修改都被施加到这个临时的拷贝体了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Node
{
public int Id;

public void Change()
{
// 任何修改都是作用域临时的拷贝副本
Id = 111;
}
}

void Test(in Node node)
{
// Rider提示:“在只读变量中调用可能不纯的结构方法;结构值始终在调用前复制”
node.Change();
}

解决方法是给结构体加上readonly关键字,或者给Change函数加上readonly关键字,杜绝无意的调用。

Ref参数

引用传递

  • 调用前必须初始化
  • 引用传递

⭐引用传递补充⭐

普通函数传参:变量值被复制

  • 值类型:拷贝一份
  • 引用类型:拷贝一份(引用对象的地址)

通过ref/in/out传参:变量本身的地址

无论值类型还是引用类型,传递的都是变量本身的地址;区别在于变量里存的是什么:

  • 值类型变量:存的是值
  • 引用类型变量:存的是对象引用

所以说:

  • 对于值类型,传递的是变量的地址
  • 对于引用类型,传递的是“引用变量本身”的地址(而不是引用对象的地址,那是普通引用传递!),类似C语言里的“指针的指针”,因此像ref和out可以在内部更改其指向新的引用数据

协变和逆变

in和out在泛型里可以用作“变形(variance)”,用于描述类型转换规则,主要用在泛型接口和泛型委托

问题

假设有如下定义

1
2
3
4
5
6
7
8
9
10
class Animal {}
class Dog : Animal {}

interface IBox<T>
{
T Get();
}

IBox<Dog> dogBox = new XXX();
IBox<Animal> animalBox = dogBox; // ×

由于父类可以指向子类(子类赋值给父类),所以我们希望子类接口可以赋值给父类接口。但是默认泛型是不允许这样的,因为可能会有类型安全问题。

只读情况

假如接口内只有一个实现T Get(),那么IBox<Dog> dogBox永远只返回DogDogAnimal,所以IBox<Dog> -> IBox<Animal>是安全的,这就是out T

读写情况

假如接口内还有一个实现void Set(T value),如果允许IBox<Dog> -> IBox<Animal>,就会发生:

1
2
3
IBox<Dog> dogBox = ...
IBox<Animal> animalBox = dogBox
animalBox.Set(new Animal());

现在dogBox装了Animal了,类型系统崩溃,因此不能允许同时读写

规则

因此C#引入了两个规则:

  • 如果只返回T,那就允许out T,表示T只从里面出来,只生产T
  • 如果只接收T,那就运行in T,表示T只流进去,只消费T

不变(Invariant)

泛型默认是不变(Invariant)的,父子之间无法转换

1
2
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // ×

协变(Covariant)

生产数据,子类可以转换为父类

接口

协变out表示只能作为返回值,

1
2
3
4
5
6
7
8
interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}

IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs; // √
// 狗的列表也是动物的列表

委托

1
2
3
4
5
public delegate TResult TheFunc<out TResult>();

TheFunc<Dog> f = () => new Dog();
TheFunc<Animal> g = f; // √
// 返回狗也是返回动物

从生产者角度理解:能生产父类,肯定也可以生产子类,外部只看父类部分

  • 接口中,返回子类的接口,也可以被返回父类的接口持有,因为父类可以指向子类,外部只知道父类不知道子类
  • 委托中,返回子类的回调,也可以绑定到返回父类的委托上,因为父类可以指向子类,外部只知道父类不知道子类

逆变(Contravariant)

消费数据,父类可以转换为子类

接口

逆变in表示只能作为参数

1
2
3
4
5
6
7
8
interface IComparer<in T>
{
int Compare(T x, T y);
}

IComparer<Animal> comparer = new AnimalComparer();
IComparer<Dog> dogComparer = comparer; // √
// 能够排序动物的,肯定能排序狗

委托

1
2
3
4
5
public delegate void TheAction<in T>(T obj);

TheAction<Animal> act = (Animal a) => {};
TheAction<Dog> dogAct = act; // √
// 只处理动物的回调,也能处理狗

从消费者角度理解:能消费父类的,肯定也能消费子类,内部只看父类部分

  • 接口中,接收父类的接口,也可以被接收子类的接口所持有,因为父类可以指向子类,内部只知道父类不知道子类
  • 委托中,接收父类的回调,也可以绑定到接收子类的委托上,因为父类可以指向子类,内部只知道父类不知道子类