不學 JAVA 換學 C# 之覺得心累 - L1:ch8 方法的進階概念

AdSense

前言

參數使我們能夠在方法之間傳遞資料。C# 提供了多種參數類型,例如值參數、參考參數、輸出參數以及參數陣列。

  • 值參數:
    • 預設行為。
    • 方法會獲得值的複製。如果是值類型 (Value types),會複製變數的值。如果是參考類型 (Reference types),會複製記憶體位址。關於變數類型請參考資料型態
  • 參考參數:
    • ref 修飾註明。
    • 取別名,重新指派會影響原始變數的資料。
    • 呼叫前需要初始化變數
    • 方法中,不一定要修改資料。
  • 輸出參數:
    • out 修飾註明。
    • 用於從方法中傳回資料,在呼叫前不需初始化變數
    • 在方法中,一定要指派資料
  • 參數陣列:
    • params 修飾註明。
    • 允許傳入不固定數量的參數

值參數 (Call/Pass by Value)

請先參考值 (value)、指標 (pointer/address)、參考 (reference)

值類型 (Value types)

會複製變數的值。

1
2
3
4
5
6
7
8
9
int x = 5, y = 10;
Swap(x, y);

void Swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}

執行 Swap 方法 line 6 ~ 8 前。

記憶體位址 變數名稱
0x30 5 x
0x40 10 y
0x938 5 a
0x948 10 b

執行完 Swap 方法後。

記憶體位址 變數名稱
0x30 5 x
0x40 10 y
0x938 10 a
0x948 5 b

參考類型 (Reference types)

會複製記憶體位址。

1
2
3
4
5
6
7
8
9
10
void ModifyArray(int[] numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] *= 2; // 每個元素乘以 2
}
}

int[] arr = { 10, 20, 30, 40 };
ModifyArray(arr);

執行 line 9 後,產生變數 arr 並創造記憶體位址。

記憶體位址 變數名稱
0x30 0x748 arr
記憶體位址 變數名稱
0x748 10 arr[0]
0x758 20 arr[1]
0x768 30 arr[2]
0x778 40 arr[3]

執行 line 10,複製 arr 的值 0x748numbers 並傳入 ModifyArray 方法中,因為 arrnumbers 都指向同一個記憶體位址,所以 arr[0] 也就是 numbers[0],以此類推,並且每個元素乘以 2。

記憶體位址 變數名稱
0x30 0x748 arr
0x40 0x748 numbers
記憶體位址 變數名稱
0x748 20 arr[0] / numbers[0]
0x758 40 arr[1] / numbers[1]
0x768 60 arr[2] / numbers[2]
0x778 80 arr[3] / numbers[3]

如果我在 ModifyArray 中重新指派陣列,就不會影響到原始陣列。

1
2
3
4
5
6
7
8
9
10
11
void ModifyArray(int[] numbers)
{
numbers = new int[] { 3, 3, 3, 3 };
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] *= 2; // 每個元素乘以 2
}
}

int[] arr = { 10, 20, 30, 40 };
ModifyArray(arr);

因為執行 line 3 時,將 numbers 的值從 0x748 換成 0x913,指向不同的陣列。

記憶體位址 變數名稱
0x30 0x748 arr
0x40 0x913 numbers
記憶體位址 變數名稱
0x748 10 arr[0]
0x758 20 arr[1]
0x768 30 arr[2]
0x778 40 arr[3]
記憶體位址 變數名稱
0x913 6 numbers[0]
0x923 6 numbers[1]
0x933 6 numbers[2]
0x943 6 numbers[3]

參考參數、輸出參數 (Call/Pass by Reference)

ref 參數修飾詞

若想要在呼叫方法中修改原始資料,可以使用參考參數 ref 。變數在呼叫前必須被初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;

namespace HelloWorld;

class Program
{
static void UpdateValue(ref int value) {
value += 10;
}

static void Main(string[] args)
{
int number = 5;
UpdateValue(ref number);
Console.WriteLine(number);
Console.ReadLine();
}
}

// 輸出:
// 15

out 參數修飾詞

如果變數會在方法內被設定,使用輸出參數 out

變數在呼叫前不必初始化,但一定要在方法內被設定,否則拋出以下錯誤。

error CS0177: The out parameter `b’ must be assigned to before control leaves the current method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;

namespace HelloWorld;

class Program
{
static void GetValues(out int x, out int y) {
x = 10;
y = 20;
}

static void Main(string[] args)
{
GetValues(out int a, out int b);
Console.WriteLine("a is {0}", a);
Console.WriteLine("b is {0}", b);
Console.ReadLine();
}
}

// 輸出:
// a is 10
// b is 20

變數在呼叫前可以被初始化,但是會被忽略,傳不進方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;

namespace HelloWorld;

class Program
{
static void OutValue(int a, out int b)
{
// int c = b; // 在設定 b 前,將 b 指派給 c,會產生錯誤
b = 333;
}

static void Main(string[] args)
{
int x = 500; // 就算有先給 value,但是無法將 value 傳進 method 中
Console.WriteLine("x can be unassigned or have initialization {0}", x);
OutValue(1, out x);
Console.WriteLine("x is {0}", x);
Console.ReadLine();
}
}

// 輸出:
// x can be unassigned or have initialization 500
// x is 333

即使 x 在呼叫 OutValue 前被初始化成 500,但是 500 無法傳給 b,因此 line 9 會產生以下錯誤:

error CS0269: Use of unassigned out parameter `b’

使用 ref 回傳參考值

  • 在方法的回傳型態是 ref 型態,例:ref int,型態跟我想回傳參考的該變數一致。
  • return 前加上 ref
  • 要接收回傳結果的變數的宣告型態前加上 ref

限制:C# 覺得安全的參考才能回傳

主要是為了避免返回無效的參考,並確保程式執行的安全性與穩定性。

  • 可以被回傳
    • 被傳進方法的變數的參考。
  • 不可以被回傳
    • 方法內部定義的臨時變數 (局部變數) 的參考。
    • 方法內部產生的值 (例如:計算結果的臨時變數) 的參考。

例子一

以下的例子中,FindRefOfValue 方法是用來找到陣列中,值等於 target 的參考 (Reference),並且回傳,讓某變數的參考等於該值,該變數就會是 target 的別名了。

在 line 21 ref FindRefOfValue(nums, 250) 去找陣列中值等於 250 的參考,也就是 nums[1] 的參考,並且回傳參考,指派給 result 的參考,result 即是 nums[1] 的別名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using System;

namespace HelloWorld;

class Program
{
// 回傳型態 ref int,因為 numbers[i] 是 int 型態
static ref int FindRefOfValue(int[] numbers, int target) {
for (int i = 0; i < numbers.Length; i++) {
if (numbers[i] == target) {
return ref numbers[i]; // return 前加上 ref
}
}
throw new Exception("Value not found");
}

static void Main()
{
int[] nums = {150, 250, 350};
// 變數 result 的宣告型態 int 前加上 ref
ref int result = ref FindRefOfValue(nums, 250);
result = 0; // 修改值,這行也就是 nums[1] = 0;
foreach (int num in nums)
{
Console.Write(num + ", ");
}
Console.ReadLine();
}
}

// 輸出:
// 150, 0, 350,

例子二

在 line 19 ref UpdateBViaRef(ref b) 中會回傳 b 的參考,指派給 c 的參考,c 即是 b 的別名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;

namespace HelloWorld;

class Program
{
// 回傳型態 ref int,因為 bb 是 int 型態
static ref int UpdateBViaRef(ref int bb)
{
bb = 50;
return ref bb; // return 前加上 ref
}

static void Main()
{
int b = 100;
Console.WriteLine("Before UpdateBViaRef: b is {0}", b);
// 變數 c 的宣告型態 int 前加上 ref
ref int c = ref UpdateBViaRef(ref b); // 等同於下面這行
// ref int c = ref b; // make a alias(c) of b // c 是 b 的別名
Console.WriteLine("After UpdateBViaRef: b is {0}", b);
Console.WriteLine("c is a alias of b (c 是 b 的別名). c is {0}\n", c);
c = 30;
Console.WriteLine("After updating c to 30: b is {0}, c is {1}\n", b, c);
b = 10;
Console.WriteLine("After updating b to 10: b is {0}, c is {1}\n", b, c);
Console.ReadLine();
}
}

// 輸出:
// Before UpdateBViaRef: b is 100
// After UpdateBViaRef: b is 50
// c is a alias of b (c 是 b 的別名). c is 50

// After updating c to 30: b is 30, c is 30

// After updating b to 10: b is 10, c is 10

多載 (Overloading)

不學 JAVA 換學 C# 之覺得心累 - L1:ch7 方法裡面有提到:具有相同名稱參數型態、個數與順序不同的方法會被視為不同的方法,這被稱為多載 (Overloading)

以下的例子中,line 1 和 lin 5 的方法是型態不同, lin 5 和 line 9 的方法是個數不同。

1
2
3
4
5
6
7
8
9
10
11
void Print(int number) {
Console.WriteLine(number);
}

void Print(string text) {
Console.WriteLine(text);
}

void Print(string text1, string text2) {
Console.WriteLine(text1, text2);
}

參數陣列

了解多載後就知道,如果我有一個方法,想要接收 2、5 或 10 個的參數,我就需要定義三個方法,第一個方法的參數列表有兩個,第二個方法的參數列表有五個,第三個方法的參數列表有十個,這樣太麻煩了,萬一要更多不固定數量的參數怎麼辦?因此就有了參數陣列。

使用 params 修飾詞來定義方法的參數,允許傳入不定數量的參數,這在需要處理多個不固定數量的參數時特別有用。

  • 只能有一個 params 參數,且必須是參數列表中的最後一個參數
  • 傳遞給 params 參數的數值會被視為一個陣列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;

namespace HelloWorld;

class Program
{
// params int[] numbers 必須是參數列表中的最後一個參數,不能在 string text 前面
static void PrintNumbers(string text, params int[] numbers) {
Console.WriteLine(text);
foreach (int number in numbers) {
Console.Write(number + ", ");
}
Console.WriteLine();
}

static void Main()
{
PrintNumbers("No any number param: ");
PrintNumbers("Five params: ", 1, 2, 3, 4, 5); // 1, 2, 3, 4, 5 被視為一個陣列
Console.ReadLine();
}
}

// 輸出:
// No any number param:
//
// Five params:
// 1, 2, 3, 4, 5,

參考資料:
方法參數和修飾符號
Difference between Ref and Out keywords in C#