C#系列 ---5 函式引數 optional , ref, out, params 和 引數值傳遞和引用傳遞問題
variables and parameters
variable 代表的是一個記憶體地址,該地址包含一個可變的值。可以是local variable, parameter (value, ref, or out), field (instance or static), or array element
棧和堆(stack and heap)
棧(stack):儲存區域性變數和引數(local variables and parameters)的記憶體塊,當函式壓棧或出棧時,棧在邏輯上會增加或縮減。典型的遞迴函式的分析
堆(heap):
- 儲存著物件(object),例如引用型別物件的例項。每當新的物件 建立,將分配到堆上,並返回物件的引用。執行時(runtime) 會有一個垃圾收集器,週期性的從堆上刪除不再使用的物件, 從而保證程式不會因為記憶體不夠而崩潰。
- 儲存static field(靜態欄位),不會被垃圾收集器收集,直到程式結束或崩潰,靜態欄位的變數才會消失。靜態變數的概念在C++中也有
Definite Assignment 顯示賦值
C#強制顯示賦值。這樣將無法使用未初始化的變數,避免程式錯誤
主要表現在:
- 區域性變數在使用前一定要初始化
- 函式在被呼叫時,函式引數必須全部傳入(除了可選引數)
- 其他的變數(比如欄位和陣列元素(fields and array elements))由執行時(runtime)自動初始化
A field is a variable of any type that is declared directly in a class or struct.
在類或結構體裡直接宣告的變數成員,除此之外也包括靜態變數
public class CalendarEntry { // private field private DateTime date; // public field (Generally not recommended.) public string day; // Public property exposes date field safely. public DateTime Date { get { return date; } set { // Set some reasonable boundaries for likely birth dates. if (value.Year > 1900 && value.Year <= DateTime.Today.Year) { date = value; } else throw new ArgumentOutOfRangeException(); } } // Public method also exposes date field safely. // Example call: birthday.SetDate("1975, 6, 30"); public void SetDate(string dateString) { DateTime dt = Convert.ToDateTime(dateString); // Set some reasonable boundaries for likely birth dates. if (dt.Year > 1900 && dt.Year <= DateTime.Today.Year) { date = dt; } else throw new ArgumentOutOfRangeException(); } }
程式碼例項:
//使用未初始化的區域性變數,出錯
static void Main()
{
int x;
Console.WriteLine (x); // Compile-time error
}
//陣列元素
static void Main()
{
int[] ints = new int[2];
Console.WriteLine (ints[0]); // 0
}
//靜態變數, 欄位
class Test
{
static int x;
static void Main() { Console.WriteLine (x); } // 0
}
預設值 Default Values
引數 parameters
引數傳遞
- 值傳遞: 在C#中,引數預設按值傳遞,即當傳給函式時,傳入的是引數的拷貝值
class Test
{
static void Foo (int p)
{
p = p + 1; // Increment p by 1
Console.WriteLine (p); // Write p to screen
}
static void Main()
{
int x = 8;
Foo (x); // Make a copy of x
Console.WriteLine (x); // x will still be 8
}
}
對於引用型別,將引用賦值,而非引用指向的物件。
class Test
{
static void Foo (StringBuilder fooSB)
{
fooSB.Append ("test");
fooSB = null;
}
static void Main()
{
StringBuilder sb = new StringBuilder();
Foo (sb);
Console.WriteLine (sb.ToString()); // test
}
}
結合上圖將不難理解,傳入的fooSb其實相當於ref2,將ref1進行拷貝,也就是ref2和ref1指向同一位置,修改ref2指向物件的內容同樣修改ref1的內容,但是將ref2重新指向null,並不會改變ref1的指向。
要是不想傳入拷貝值,而是傳入原始值呢?
使用引用傳遞,使用關鍵字:ref
class Test
{
static void Foo (ref int p)
{
p = p + 1; // Increment p by 1
Console.WriteLine (p); // Write p to screen
}
static void Main()
{
int x = 8;
Foo (ref x); // Ask Foo to deal directly with x
Console.WriteLine (x); // x is now 9
}
}
使用ref實現交換函式:
class Test
{
static void Swap (ref string a, ref string b)
{
string temp = a;
a = b;
b = temp;
}
static void Main()
{
string x = "Penn";
string y = "Teller";
Swap (ref x, ref y);
Console.WriteLine (x); // Teller
Console.WriteLine (y); // Penn
}
}
關鍵字 out
也是用在引數前面,用來承接多個返回值, 與ref一樣也是引用傳遞
但是,
- 在傳入函式時不需要初始化
- 在函式退出時,必須完成賦值
結合例子理解一下:
class Test
{
static void Split (string name, out string firstNames,
out string lastName)
{
int i = name.LastIndexOf (' ');
firstNames = name.Substring (0, i);
lastName = name.Substring (i + 1);
}
static void Main()
{
string a, b;
Split ("Stevie Ray Vaughan", out a, out b);
Console.WriteLine (a); // Stevie Ray
Console.WriteLine (b); // Vaughan
}
}
C# 7新增的
- 動態宣告out的變數型別
static void Main()
{
Split ("Stevie Ray Vaughan", out string a, out string b);
Console.WriteLine (a); // Stevie Ray
Console.WriteLine (b); // Vaughan
}
- 使用
out _
表示捨棄該變數
static void Main()
{
Split ("Stevie Ray Vaughan", out string a, out _);// Discard the 2nd param
Console.WriteLine (a);
}
但是,為了向後相容性,
以下的語法將報錯
string _;
Split ("Stevie Ray Vaughan", out string a, _); // Will not compile
引用傳遞的含義
通過引用傳遞引數,簡單的理解是傳入原始值,而非傳入拷貝值,這意味著兩個完全相同。代表完全同一個例項。
比如下面程式碼中x和y代表同一個例項
class Test
{
static int x;
static void Main() { Foo (out x); }
static void Foo (out int y)
{
Console.WriteLine (x); // x is 0
y = 1; // Mutate y
Console.WriteLine (x); // x is 1
}
}
關鍵字 params
params可以在函式的最後一個引數上指定,可以接受特定型別的任意數量的引數,
引數型別必須宣告為陣列型別
class Test
{
static int Sum (params int[] ints)
{
int sum = 0;
for (int i = 0; i < ints.Length; i++)
sum += ints[i]; // Increase sum by ints[i]
return sum;
}
static void Main()
{
int total = Sum (1, 2, 3, 4);
Console.WriteLine (total); // 10
}
}
也可以將params引數看做一個普通的陣列,此時,傳入一個數組即可,比如:
下面的程式碼與Main
中的第一行程式碼等效:
int total = Sum (new int[] { 1, 2, 3, 4 } );
optional parameters 可選引數
named parameters
可以再傳遞引數時使用名字來指定特定的引數
void Foo (int x, int y) { Console.WriteLine (x + ", " + y); }
void Test()
{
Foo (x:1, y:2); // 1, 2
}
但是,在傳遞引數時,位置引數一定要在命名的引數前面
Foo (x:1, 2); // Compile-time error
可選引數(optional parameters)和命名引數(named parameters)結合在一起使用,很有效果。
比如:
void Bar (int a = 0, int b = 0, int c = 0, int d = 0) { ... }
Bar(d:3) //僅僅改變d的值,而其他的引數值不用改變
var Implicitly Typed Local Variables 隱式的區域性型別變數
如果編譯器能夠從初始化語句中推斷出變數的型別,可以使用關鍵字var
var x = "hello";
var y = new System.Text.StringBuilder();
var z = (float)Math.PI;
//等效於
string x = "hello";
System.Text.StringBuilder y = new System.Text.StringBuilder();
float z = (float)Math.PI;
但是,使用var
是在靜態編譯時就確定變數的具體型別了,所以以下程式碼會報錯的:
var x = 5;
x = "hello"; // Compile-time error; x is of type int
我覺得吧,var嚴重降低了程式碼的可讀性
Expressions and Operators
null operators
string s1 = null;
string s2 = s1 ?? "nothing"; // s2 evaluates to "nothing"
如果s1不是null,賦值給s2,否則將預設值’nothong’賦值給s2