1. 程式人生 > >C#系列 ---5 函式引數 optional , ref, out, params 和 引數值傳遞和引用傳遞問題

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新增的

  1. 動態宣告out的變數型別
static void Main()
{
Split ("Stevie Ray Vaughan", out string a, out string b);
Console.WriteLine (a); // Stevie Ray
Console.WriteLine (b); // Vaughan
}

  1. 使用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