前阵子我也不知道在看哪边的代码,突然发现有个关键字in,我在想C#什么时候多出来了这个关键字。一查之下,原来是C#7.2的新特性,难怪以前从来没见过呢。那么这个新特性有什么用呢?查来查去,发现这东西和ref/out关键字很类似,所以这篇博客干脆将他们放在一起进行记录。
首先我们都知道C#中有引用类型和值类型,引用类型包含了一个数据存储在内存中的引用,其在堆内存(heap)中,生命周期比较长,并且可以有多个变量指向同一个引用,对象即是此种类型的典型。而值类型包含的是数据本身而并非引用,其生命周期比较短,通常存放在栈内存(stack)中,Int32、Struct、Double这些都是值类型中的代表。
一个方法在传入的parameter为引用类型时,传入的是引用的一份拷贝,而不是parameter的真实数据。如果你在方法内改变了parameter的数据,那么外部的数据也会被改变。然而,当你在方法内部为parameter赋值一个新的对象时,你并没有改变外部的数据,而是在改变方法内的本地数据而已。看下面的代码就知道我在说什么了:
void Start()
{
Colleague col = new Colleague();
CreateColleague(col);
Debug.Log(col.Name + " " + col.Sex);
}
public void CreateColleague(Colleague c){
c.Name = "Mike";
c.Sex = "male";
c = new Colleague();//这里不会改变外部的col变量,所以外面打印出来的结果是Mike和male
c.Name = "Nacy";
c.Sex = "female";
}
public class Colleague{
private string name;
public string Name{
get{
return name;
}
set{
name = value;
}
}
private string sex;
public string Sex{
get{
return sex;
}
set{
sex = value;
}
}
public Colleague(){
}
}
那么,可不可以在方法中赋值一个新的对象并且将外部的变量改变了呢?答案是肯定的,加个ref关键字就行了,不过谨记要先初始化好。如下代码:
void Start()
{
Colleague col = new Colleague();
CreateRefColleague(ref col);
Debug.Log(col.Name + " " + col.Sex);//这里打印出来的结果就是Nacy和female了
}
public void CreateRefColleague(ref Colleague c){
c.Name = "Mike";
c.Sex = "male";
c = new Colleague();
c.Name = "Nacy";
c.Sex = "female";
}
public class Colleague{
private string name;
public string Name{
get{
return name;
}
set{
name = value;
}
}
private string sex;
public string Sex{
get{
return sex;
}
set{
sex = value;
}
}
public Colleague(){
}
}
并且,ref关键字也适用于值类型的parameter。如下代码所示:
void Start()
{
int n = 1;
IncrementInt(ref n);
Debug.Log(n);//n变成了2
}
public void IncrementInt(ref int n){
n++;
}
out关键字与ref的使用很相似,不过通常来讲out关键字修饰的变量不先进行初始化,而是在方法中初始化它。
void Start()
{
Colleague co;//不初始化变量
CreateOutColleague(out co);//将变量放进方法中初始化
Debug.Log(co.Name + " " + co.Sex);
}
public void CreateOutColleague(out Colleague c){
c = new Colleague();
c.Name = "Mike";
c.Sex = "male";
}
public class Colleague{
private string name;
public string Name{
get{
return name;
}
set{
name = value;
}
}
private string sex;
public string Sex{
get{
return sex;
}
set{
sex = value;
}
}
public Colleague(){
}
}
而in关键字就很有意思了,它也像ref一样需要初始化好,但是它的用处是不让方法内部对其进行赋值新的对象,如下代码:
void Start()
{
Colleague col = new Colleague();
CreateInColleague(col);
Debug.Log(col.Name + " " + col.Sex);
}
public void CreateInColleague(in Colleague c){
//c = new Colleague();//compile error
c.Name = "Mike";
c.Sex = "male";
}
public class Colleague{
private string name;
public string Name{
get{
return name;
}
set{
name = value;
}
}
private string sex;
public string Sex{
get{
return sex;
}
set{
sex = value;
}
}
public Colleague(){
}
}
要是在方法内执行c = new Colleague();
的话就会抛出如下错误
那么这个新特性in关键字除了这个用处还有什么用呢?我查阅了一些资料发现,当下似乎只有一个用处,那就是这样
readonly struct VeryLarge
{
public readonly long Value1;
public readonly long Value2;
public long Compute() { }
// etc
}
void Process(in VeryLarge value) { }
当你有一个很大的结构体并且这个结构体是readonly
的时候(readonly
结构体也是C#7.2的新特性),在方法中使用in关键字可以使得这个方法被高效的执行,因为其防止了defensive copy的发生。而如果使用了in关键字却没有让结构体是readonly
的情况下,defensive copy将会发生,影响性能!
这里的这个defensive copy是什么意思呢?原来,编译器为了防止任何潜在的在方法内改变readonly
变量(像这里的readonly long Value1/Value2)的可能,会有一个防御性的拷贝,将原来的readonly
变量拷贝出来,对这个变量操作几次就拷贝几次,可想而知这对性能会产生多大影响。而有了in关键字,这个问题就被解决了!
当然,这三个关键字也有不能使用的时候,一种情况是async修饰的方法里不能用,你可以绕个弯,在同步方法内返回一个Task,这样的方法内是可以用的。另一种情况是迭代器方法(iterator method)中有yield return
或者yield break
的情况下不能用。
在overloading时,有上述任何关键字方法的签名都会和没有上述关键字方法的签名不同,而如果只是关键字的不同,编译器会报错。
编译报错最后总结:
- ref是表明parameter可能会被方法所改变,需要初始化。
- out是表明parameter一定会被方法所改变,不需要初始化。
- in是表明parameter不能被方法所改变,需要初始化。
参考
C# - Using in, out, and Ref with Parameters
C# - Passing a Reference vs. Value
Why would one ever use the “in” parameter modifier in C#?
The ‘in’-modifier and the readonly structs in C#
网友评论