iCsharp in & out & ref
参数传递
Out参数
只用于输出结果
- 调用前不需要初始化
- 方法内部必须赋值
- 引用传递
In参数
引用传递+只读。主要用途是传参时避免大型struct拷贝,并安全的只读访问
- 调用前必须初始化
- 方法内部只读
- 引用传递
- 传参时可以省略,编译器自动加,但是不建议省略
⭐隐式拷贝⭐
如果结构体不是readonly的,在通过in传递到只读上下文时,如果调用了该函数体内部的某个方法Change,而这个方法会修改结构体内部数据,此时Change会通过拷贝一份该函数体再进行调用,Change里任何对原结构体的修改都被施加到这个临时的拷贝体了。
1 | struct Node |
解决方法是给结构体加上readonly关键字,或者给Change函数加上readonly关键字,杜绝无意的调用。
Ref参数
引用传递。
- 调用前必须初始化
- 引用传递
⭐引用传递补充⭐
普通函数传参:变量值被复制
- 值类型:拷贝一份
- 引用类型:拷贝一份(引用对象的地址)
通过ref/in/out传参:变量本身的地址
无论值类型还是引用类型,传递的都是变量本身的地址;区别在于变量里存的是什么:
- 值类型变量:存的是值
- 引用类型变量:存的是对象引用
所以说:
- 对于值类型,传递的是变量的地址
- 对于引用类型,传递的是“引用变量本身”的地址(而不是引用对象的地址,那是普通引用传递!),类似C语言里的“指针的指针”,因此像ref和out可以在内部更改其指向新的引用数据
协变和逆变
in和out在泛型里可以用作“变形(variance)”,用于描述类型转换规则,主要用在泛型接口和泛型委托上
问题
假设有如下定义
1 | class Animal {} |
由于父类可以指向子类(子类赋值给父类),所以我们希望子类接口可以赋值给父类接口。但是默认泛型是不允许这样的,因为可能会有类型安全问题。
只读情况
假如接口内只有一个实现T Get(),那么IBox<Dog> dogBox永远只返回Dog而Dog是Animal,所以IBox<Dog> -> IBox<Animal>是安全的,这就是out T
读写情况
假如接口内还有一个实现void Set(T value),如果允许IBox<Dog> -> IBox<Animal>,就会发生:
1 | IBox<Dog> dogBox = ... |
现在dogBox装了Animal了,类型系统崩溃,因此不能允许同时读写
规则
因此C#引入了两个规则:
- 如果只返回
T,那就允许out T,表示T只从里面出来,只生产T - 如果只接收
T,那就运行in T,表示T只流进去,只消费T
不变(Invariant)
泛型默认是不变(Invariant)的,父子之间无法转换
1 | List<Dog> dogs = new List<Dog>(); |
协变(Covariant)
生产数据,子类可以转换为父类
接口
协变out表示只能作为返回值,
1 | interface IEnumerable<out T> |
委托
1 | public delegate TResult TheFunc<out TResult>(); |
从生产者角度理解:能生产父类,肯定也可以生产子类,外部只看父类部分
- 接口中,返回子类的接口,也可以被返回父类的接口持有,因为父类可以指向子类,外部只知道父类不知道子类
- 委托中,返回子类的回调,也可以绑定到返回父类的委托上,因为父类可以指向子类,外部只知道父类不知道子类
逆变(Contravariant)
消费数据,父类可以转换为子类
接口
逆变in表示只能作为参数
1 | interface IComparer<in T> |
委托
1 | public delegate void TheAction<in T>(T obj); |
从消费者角度理解:能消费父类的,肯定也能消费子类,内部只看父类部分
- 接口中,接收父类的接口,也可以被接收子类的接口所持有,因为父类可以指向子类,内部只知道父类不知道子类
- 委托中,接收父类的回调,也可以绑定到接收子类的委托上,因为父类可以指向子类,内部只知道父类不知道子类