前陣子我也不知道在看哪邊的代碼,突然發(fā)現(xiàn)有個(gè)關(guān)鍵字in,我在想C#什么時(shí)候多出來(lái)了這個(gè)關(guān)鍵字。一查之下,原來(lái)是C#7.2的新特性,難怪以前從來(lái)沒(méi)見(jiàn)過(guò)呢。那么這個(gè)新特性有什么用呢?查來(lái)查去,發(fā)現(xiàn)這東西和ref/out關(guān)鍵字很類(lèi)似,所以這篇博客干脆將他們放在一起進(jìn)行記錄。
首先我們都知道C#中有引用類(lèi)型和值類(lèi)型,引用類(lèi)型包含了一個(gè)數(shù)據(jù)存儲(chǔ)在內(nèi)存中的引用,其在堆內(nèi)存(heap)中,生命周期比較長(zhǎng),并且可以有多個(gè)變量指向同一個(gè)引用,對(duì)象即是此種類(lèi)型的典型。而值類(lèi)型包含的是數(shù)據(jù)本身而并非引用,其生命周期比較短,通常存放在棧內(nèi)存(stack)中,Int32、Struct、Double這些都是值類(lèi)型中的代表。
一個(gè)方法在傳入的parameter為引用類(lèi)型時(shí),傳入的是引用的一份拷貝,而不是parameter的真實(shí)數(shù)據(jù)。如果你在方法內(nèi)改變了parameter的數(shù)據(jù),那么外部的數(shù)據(jù)也會(huì)被改變。然而,當(dāng)你在方法內(nèi)部為parameter賦值一個(gè)新的對(duì)象時(shí),你并沒(méi)有改變外部的數(shù)據(jù),而是在改變方法內(nèi)的本地?cái)?shù)據(jù)而已??聪旅娴拇a就知道我在說(shuō)什么了:
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();//這里不會(huì)改變外部的col變量,所以外面打印出來(lái)的結(jié)果是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(){
}
}
那么,可不可以在方法中賦值一個(gè)新的對(duì)象并且將外部的變量改變了呢?答案是肯定的,加個(gè)ref關(guān)鍵字就行了,不過(guò)謹(jǐn)記要先初始化好。如下代碼:
void Start()
{
Colleague col = new Colleague();
CreateRefColleague(ref col);
Debug.Log(col.Name + " " + col.Sex);//這里打印出來(lái)的結(jié)果就是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關(guān)鍵字也適用于值類(lèi)型的parameter。如下代碼所示:
void Start()
{
int n = 1;
IncrementInt(ref n);
Debug.Log(n);//n變成了2
}
public void IncrementInt(ref int n){
n++;
}
out關(guān)鍵字與ref的使用很相似,不過(guò)通常來(lái)講out關(guān)鍵字修飾的變量不先進(jìn)行初始化,而是在方法中初始化它。
void Start()
{
Colleague co;//不初始化變量
CreateOutColleague(out co);//將變量放進(jìn)方法中初始化
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關(guān)鍵字就很有意思了,它也像ref一樣需要初始化好,但是它的用處是不讓方法內(nèi)部對(duì)其進(jìn)行賦值新的對(duì)象,如下代碼:
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(){
}
}
要是在方法內(nèi)執(zhí)行c = new Colleague();的話就會(huì)拋出如下錯(cuò)誤

那么這個(gè)新特性in關(guān)鍵字除了這個(gè)用處還有什么用呢?我查閱了一些資料發(fā)現(xiàn),當(dāng)下似乎只有一個(gè)用處,那就是這樣
readonly struct VeryLarge
{
public readonly long Value1;
public readonly long Value2;
public long Compute() { }
// etc
}
void Process(in VeryLarge value) { }
當(dāng)你有一個(gè)很大的結(jié)構(gòu)體并且這個(gè)結(jié)構(gòu)體是readonly的時(shí)候(readonly結(jié)構(gòu)體也是C#7.2的新特性),在方法中使用in關(guān)鍵字可以使得這個(gè)方法被高效的執(zhí)行,因?yàn)槠浞乐沽薲efensive copy的發(fā)生。而如果使用了in關(guān)鍵字卻沒(méi)有讓結(jié)構(gòu)體是readonly的情況下,defensive copy將會(huì)發(fā)生,影響性能!
這里的這個(gè)defensive copy是什么意思呢?原來(lái),編譯器為了防止任何潛在的在方法內(nèi)改變readonly變量(像這里的readonly long Value1/Value2)的可能,會(huì)有一個(gè)防御性的拷貝,將原來(lái)的readonly變量拷貝出來(lái),對(duì)這個(gè)變量操作幾次就拷貝幾次,可想而知這對(duì)性能會(huì)產(chǎn)生多大影響。而有了in關(guān)鍵字,這個(gè)問(wèn)題就被解決了!
當(dāng)然,這三個(gè)關(guān)鍵字也有不能使用的時(shí)候,一種情況是async修飾的方法里不能用,你可以繞個(gè)彎,在同步方法內(nèi)返回一個(gè)Task,這樣的方法內(nèi)是可以用的。另一種情況是迭代器方法(iterator method)中有yield return或者yield break的情況下不能用。
在overloading時(shí),有上述任何關(guān)鍵字方法的簽名都會(huì)和沒(méi)有上述關(guān)鍵字方法的簽名不同,而如果只是關(guān)鍵字的不同,編譯器會(huì)報(bào)錯(cuò)。

最后總結(jié):
- ref是表明parameter可能會(huì)被方法所改變,需要初始化。
- out是表明parameter一定會(huì)被方法所改變,不需要初始化。
- 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#