訓練
C# 中的方法
方法是包含一系列陳述式的程式碼區塊。 程式會造成呼叫方法並指定任何所需的方法引數來執行陳述式。 在 C# 中,每個執行的指示是在方法的內容中執行。
注意
本主題討論具名的方法。 如需匿名函式的資訊,請參閱 Lambda 運算式。
指定下列項目以 class
、record
或 struct
宣告方法:
- 選擇性的存取層級,例如
public
或private
。 預設值為private
。 - 選擇性修飾詞,例如
abstract
或sealed
。 - 傳回值,或如果方法為無,則為
void
。 - 方法名稱。
- 任何方法參數。 方法參數會放在括號中,並以逗號分隔。 空括號表示方法不需要任何參數。
這些組件一起構成方法簽章。
重要
方法的傳回類型不是方法多載用途的方法簽章的一部分。 不過,在判斷委派與所指向的方法之間的相容性時,它是方法簽章的一部分。
下例定義名為 Motorcycle
的類別,包含五個方法:
namespace MotorCycleExample
{
abstract class Motorcycle
{
// Anyone can call this.
public void StartEngine() {/* Method statements here */ }
// Only derived classes can call this.
protected void AddGas(int gallons) { /* Method statements here */ }
// Derived classes can override the base class implementation.
public virtual int Drive(int miles, int speed) { /* Method statements here */ return 1; }
// Derived classes can override the base class implementation.
public virtual int Drive(TimeSpan time, int speed) { /* Method statements here */ return 0; }
// Derived classes must implement this.
public abstract double GetTopSpeed();
}
Motorcycle
類別包含多載方法 Drive
。 兩種方法有相同的名稱,但是依其參數型別區分。
方法可以是「執行個體」或「靜態」。 您必須具現化物件以針對該執行個體叫用執行個體方法,執行個體方法會在該執行個體及其資料上運作。 您可以參考方法所屬的類型名稱來叫用靜態方法,靜態方法不操作執行個體資料。 嘗試透過物件執行個體呼叫靜態方法會產生編譯器錯誤。
呼叫方法就像是存取欄位。 在物件名稱後 (如果呼叫的是執行個體方法) 或型別名稱後 (如果呼叫的是 static
方法),加上句點、方法名稱及括號。 引數會在括號中列出,並以逗號分隔。
方法定義會指定所需的任何參數的名稱和類型。 在呼叫端叫用方法時,它會針對每個參數提供具體值及呼叫的引數。 引數必須與參數型別相容,但在呼叫程式碼中使用的引數,其引數名稱不需要與方法中定義的具名參數相同。 在下例中,Square
方法包含名為 int
之 型別的單一參數。 第一個方法呼叫會傳遞給 Square
方法型別 int
的 num 變數,第二個傳遞數值常數,第三個傳遞運算式。
public static class SquareExample
{
public static void Main()
{
// Call with an int variable.
int num = 4;
int productA = Square(num);
// Call with an integer literal.
int productB = Square(12);
// Call with an expression that evaluates to int.
int productC = Square(productA * 3);
}
static int Square(int i)
{
// Store input argument in a local variable.
int input = i;
return input * input;
}
}
最常見的方法引動過程形式過去使用位置引數,現在則依方法參數的順序來提供引數。 因此可以如下列範例所示呼叫 Motorcycle
類別的方法。 例如,呼叫 Drive
方法包含兩個引數,它們會對應至方法語法的兩個參數。 第一個會成為 miles
參數的值。 第二個會成為 speed
參數的值。
class TestMotorcycle : Motorcycle
{
public override double GetTopSpeed() => 108.4;
static void Main()
{
var moto = new TestMotorcycle();
moto.StartEngine();
moto.AddGas(15);
_ = moto.Drive(5, 20);
double speed = moto.GetTopSpeed();
Console.WriteLine("My top speed is {0}", speed);
}
}
叫用方法時,您也可以使用具名引數,而不是使用位置引數。 使用具名引數時,您指定參數名稱,後面接著冒號 (":") 和引數。 方法的引數會以任意順序出現,只要有所有必要的引數。 下例使用具名引數來叫用 TestMotorcycle.Drive
方法。 本例中,具名引數的傳遞順序與方法參數清單的順序相反。
namespace NamedMotorCycle;
class TestMotorcycle : Motorcycle
{
public override int Drive(int miles, int speed) =>
(int)Math.Round((double)miles / speed, 0);
public override double GetTopSpeed() => 108.4;
static void Main()
{
var moto = new TestMotorcycle();
moto.StartEngine();
moto.AddGas(15);
int travelTime = moto.Drive(miles: 170, speed: 60);
Console.WriteLine("Travel time: approx. {0} hours", travelTime);
}
}
// The example displays the following output:
// Travel time: approx. 3 hours
您可以使用位置引數和具名引數來叫用方法。 但僅當具名參數位於正確位置時,位置參數才能遵循具名參數。 下例會使用一個位置引數和一個具名引數,從前一個範例叫用 TestMotorcycle.Drive
方法。
int travelTime = moto.Drive(170, speed: 55);
除了在型別中明確定義的成員外,型別會繼承在其基底類別中定義的成員。 因為受管理的類型系統中之所有類型,都是直接或間接繼承自 Object 類別,所以所有的類型都會繼承其成員,例如 Equals(Object)、GetType() 及 ToString()。 下例定義 Person
類別、具現化兩個 Person
物件,並呼叫 Person.Equals
方法以判斷兩個物件是否相等。 但是 Equals
方法不是在 Person
類別中定義,它繼承自 Object。
public class Person
{
public string FirstName = default!;
}
public static class ClassTypeExample
{
public static void Main()
{
Person p1 = new() { FirstName = "John" };
Person p2 = new() { FirstName = "John" };
Console.WriteLine("p1 = p2: {0}", p1.Equals(p2));
}
}
// The example displays the following output:
// p1 = p2: False
型別可以使用 override
關鍵字並提供覆寫方法的實作,來覆寫繼承的成員。 方法簽章必須與覆寫方法相同。 下例與前一範例相似,不同之處在於它會覆寫 Equals(Object) 方法。 (它也會覆寫 GetHashCode() 方法,因為兩種方法都是為了提供一致的結果。)
namespace methods;
public class Person
{
public string FirstName = default!;
public override bool Equals(object? obj) =>
obj is Person p2 &&
FirstName.Equals(p2.FirstName);
public override int GetHashCode() => FirstName.GetHashCode();
}
public static class Example
{
public static void Main()
{
Person p1 = new() { FirstName = "John" };
Person p2 = new() { FirstName = "John" };
Console.WriteLine("p1 = p2: {0}", p1.Equals(p2));
}
}
// The example displays the following output:
// p1 = p2: True
C# 中的類型為「實值型別」「參考型別」。 如需內建實值型別清單,請參閱型別。 根據預設,實值型別和參考型別都會以傳值方式傳遞至方法。
以傳值方式將實值型別傳遞至方法時,會將物件複本而不是物件本身傳遞至方法。 因此,當控制回到呼叫端時,呼叫的方法中的物件變更不會影響原始物件。
下例會以傳值方式將實值型別傳遞至方法,而呼叫的方法會嘗試變更實值型別的值。 它會定義 int
型別的變數 (它是實值型別)、將其值初始化為 20,再將它傳遞給名為 ModifyValue
的方法,此方法會將變數值變更為 30。 但傳回方法時,變數的值會維持不變。
public static class ByValueExample
{
public static void Main()
{
var value = 20;
Console.WriteLine("In Main, value = {0}", value);
ModifyValue(value);
Console.WriteLine("Back in Main, value = {0}", value);
}
static void ModifyValue(int i)
{
i = 30;
Console.WriteLine("In ModifyValue, parameter value = {0}", i);
return;
}
}
// The example displays the following output:
// In Main, value = 20
// In ModifyValue, parameter value = 30
// Back in Main, value = 20
當參考型別的物件以傳值方式傳遞至方法時,就會以傳值方式傳遞物件參考。 也就是說,此方法接收的不是物件本身,而是指出物件位置的引數。 如果您使用此參考來變更物件成員,當控制回到呼叫方法時,變更即會反映在物件中。 不過,當控制回到呼叫端時,取代傳遞至方法的物件不會影響原始物件。
下例會定義名為SampleRefType
的類別 (此為參考型別)。 它會具現化 SampleRefType
物件、將 44 指派給其 value
欄位,再將物件傳遞至 ModifyObject
方法。 本例會執行基本上與上一個範例相同的動作,依傳值方式將引數傳遞至方法。 但因為使用了參考型別,所以結果會不同。 在 ModifyObject
中對 obj.value
欄位的修改,也會將 value
方法中引數 rt
的 Main
欄位變更成 33,如範例輸出所示。
public class SampleRefType
{
public int value;
}
public static class ByRefTypeExample
{
public static void Main()
{
var rt = new SampleRefType { value = 44 };
ModifyObject(rt);
Console.WriteLine(rt.value);
}
static void ModifyObject(SampleRefType obj) => obj.value = 33;
}
當您想要變更方法中的引數值,且想要在控制回到呼叫方法時反映該變更時,您必須以傳址方式傳遞參數。 若要以傳址方式傳遞參數,請使用 ref
或 out
關鍵字。 您也可以傳址方式傳遞值,以避免發生複製的情況,但仍會無法使用 in
關鍵字進行修改。
下列範例與前一個範例相同,唯一差異是值藉傳址方式傳遞至 ModifyValue
方法。 在 ModifyValue
方法中修改參數值時,當控制回到呼叫端時會反映值的變更。
public static class ByRefExample
{
public static void Main()
{
var value = 20;
Console.WriteLine("In Main, value = {0}", value);
ModifyValue(ref value);
Console.WriteLine("Back in Main, value = {0}", value);
}
private static void ModifyValue(ref int i)
{
i = 30;
Console.WriteLine("In ModifyValue, parameter value = {0}", i);
return;
}
}
// The example displays the following output:
// In Main, value = 20
// In ModifyValue, parameter value = 30
// Back in Main, value = 30
依 ref 參數使用的常見模式包含交換變數值。 您以傳址方式將兩個變數傳遞至方法,且該方法會交換其內容。 下例會交換整數值。
public static class RefSwapExample
{
static void Main()
{
int i = 2, j = 3;
Console.WriteLine("i = {0} j = {1}", i, j);
Swap(ref i, ref j);
Console.WriteLine("i = {0} j = {1}", i, j);
}
static void Swap(ref int x, ref int y) =>
(y, x) = (x, y);
}
// The example displays the following output:
// i = 2 j = 3
// i = 3 j = 2
傳遞參考型別參數可讓您變更參考本身的值,而不是其個別項目或欄位的值。
有時候,指定方法之引數確切數目的需求會有限制。 使用 params
關鍵字指出某參數是參數集合,可使用數目可變的引數來呼叫方法。 以 params
關鍵字標記的參數必須是集合型別,而且必須是方法參數清單中的最後一個參數。
然後呼叫端會以 params
參數的下列四種方式之一叫用方法:
- 傳遞包含所需項目數目的適當型別集合。 此範例使用 集合運算式,讓編譯器建立適當的集合型別。
- 將適當型別各個引數的逗點分隔清單傳遞給方法。 編譯器會建立適當的集合型別。
- 藉由傳遞
null
。 - 不提供引數給參數集合。
下列範例定義能從參數集合中傳回所有母音,名為 GetVowels
的方法。
Main
方法會示範這四種叫用方法的方法。 呼叫端不需要針對包含 params
修飾詞的參數提供任何引數。 在此情況下,參數會是空集合。
static class ParamsExample
{
static void Main()
{
string fromArray = GetVowels(["apple", "banana", "pear"]);
Console.WriteLine($"Vowels from collection expression: '{fromArray}'");
string fromMultipleArguments = GetVowels("apple", "banana", "pear");
Console.WriteLine($"Vowels from multiple arguments: '{fromMultipleArguments}'");
string fromNull = GetVowels(null);
Console.WriteLine($"Vowels from null: '{fromNull}'");
string fromNoValue = GetVowels();
Console.WriteLine($"Vowels from no value: '{fromNoValue}'");
}
static string GetVowels(params IEnumerable<string>? input)
{
if (input == null || !input.Any())
{
return string.Empty;
}
char[] vowels = ['A', 'E', 'I', 'O', 'U'];
return string.Concat(
input.SelectMany(
word => word.Where(letter => vowels.Contains(char.ToUpper(letter)))));
}
}
// The example displays the following output:
// Vowels from array: 'aeaaaea'
// Vowels from multiple arguments: 'aeaaaea'
// Vowels from null: ''
// Vowels from no value: ''
在 C# 13 之前,params
修飾元只能與單一維度陣列搭配使用。
方法定義可以指定其參數為必要項目或選擇項目。 根據預設,參數為必要項目。 在方法定義中包含參數的預設值即可指定選擇性參數。 呼叫此方法時,如不為選擇性參數提供任何引數,則改用預設值。
您可以使用下列其中一種運算式指派參數的預設值:
常數,例如常值字串或數字。
格式
default(SomeType)
的運算式,其中SomeType
可以是實值型別或參考型別。 如果是參考型別,則實際上與指定null
相同。 您可以使用default
常值,因為編譯器可以從參數的宣告推斷類型。new ValType()
形式的運算式,其中ValType
是實值型別。 此運算式會叫用實值型別的隱含無參數建構函式,它不是該型別的實際成員。
如果方法同時包含必要和選擇性參數,則選擇性參數會定義在參數清單結尾,在所有必要參數的後面。
下例會定義 ExampleMethod
方法,它有一個必要參數和兩個選擇性參數。
public class Options
{
public void ExampleMethod(int required, int optionalInt = default,
string? description = default)
{
var msg = $"{description ?? "N/A"}: {required} + {optionalInt} = {required + optionalInt}";
Console.WriteLine(msg);
}
}
呼叫端必須針對所有選擇性參數提供引數,直到提供引數的最後一個選擇性參數。 在 ExampleMethod
方法中,例如當呼叫端為 description
參數提供引數時,它也必須為 optionalInt
參數提供一個引數。
opt.ExampleMethod(2, 2, "Addition of 2 and 2");
是有效的方法呼叫,而 opt.ExampleMethod(2, , "Addition of 2 and 0");
會產生「引數遺失」編譯器錯誤。
如果使用具名引數或位置和具名引數的組合來呼叫方法,則呼叫端可以省略方法呼叫中最後一個位置引數之後的任何引數。
下例呼叫三次 ExampleMethod
方法。 前兩個方法呼叫使用位置引數。 第一個省略了這兩個選擇性引數,而第二個省略了最後一個引數。 第三個方法呼叫提供了必要參數的位置引數,但在使用具名引數將值提供給 description
參數時省略 optionalInt
引數。
public static class OptionsExample
{
public static void Main()
{
var opt = new Options();
opt.ExampleMethod(10);
opt.ExampleMethod(10, 2);
opt.ExampleMethod(12, description: "Addition with zero:");
}
}
// The example displays the following output:
// N/A: 10 + 0 = 10
// N/A: 10 + 2 = 12
// Addition with zero:: 12 + 0 = 12
使用選擇性參數會影響「多載解析」,或 C# 編譯器判斷要針對方法呼叫叫用哪個多載的方式,如下所示:
- 如果每個參數都是依名稱或依位置對應到單一引數,且該引數可以轉換成參數的型別,則方法、索引子或建構函式就是執行的候選項目。
- 如果找到多個候選項目,則慣用轉換的多載解析規則會套用至明確指定的引數。 會忽略選擇性參數的省略引數。
- 如果兩個候選項目的評斷結果一樣好,則偏向沒有選擇性參數的候選項目,其會在呼叫中省略引數。
方法可以傳回值給呼叫者。 如果傳回型別(型別在方法名稱前面)不是 void
,則方法可以使用 return
關鍵字傳回值。 在 return
關鍵字後面接著符合傳回型別的變數、常數或運算式的陳述式,會將該值傳回給方法呼叫端。 具有非 void 傳回型別的方法需要使用 return
關鍵字以傳回值。
return
關鍵字也會停止執行方法。
如果傳回類型為 void
,不含值的 return
陳述式對於停止方法的執行仍很有用。 若沒有 return
關鍵字,在方法到達程式碼區塊的結尾時,方法就會停止執行。
例如,這兩種方法使用 return
關鍵字傳回整數:
class SimpleMath
{
public int AddTwoNumbers(int number1, int number2) =>
number1 + number2;
public int SquareANumber(int number) =>
number * number;
}
上述範例是運算式主體成員。 運算式主體成員會傳回運算式所傳回的值。
您也可以選擇使用陳述式主體和 return
陳述式來定義方法:
class SimpleMathExtnsion
{
public int DivideTwoNumbers(int number1, int number2)
{
return number1 / number2;
}
}
若要使用從方法傳回的值,呼叫方法可以在使用相同類型值的任意位置使用方法呼叫本身即已足夠。 您也可以指派傳回值給變數。 例如,下列三個程式碼範例會達到相同的目標:
int result = obj.AddTwoNumbers(1, 2);
result = obj.SquareANumber(result);
// The result is 9.
Console.WriteLine(result);
result = obj.SquareANumber(obj.AddTwoNumbers(1, 2));
// The result is 9.
Console.WriteLine(result);
result = obj2.DivideTwoNumbers(6,2);
// The result is 3.
Console.WriteLine(result);
有時候,您希望自己的方法傳回的不止單一值。 您可以使用「元組型別」和「元組常值」來傳回多個值。 Tuple 型別會定義 Tuple 項目的資料類型。 Tuple 常值會提供傳回 Tuple 的實際值。 在下列範例中,(string, string, string, int)
會定義由 GetPersonalInfo
方法所傳回的元組型別。 運算式 (per.FirstName, per.MiddleName, per.LastName, per.Age)
是元組常值,方法會傳回 PersonInfo
物件的名字、中間名和姓氏以及年齡。
public (string, string, string, int) GetPersonalInfo(string id)
{
PersonInfo per = PersonInfo.RetrieveInfoById(id);
return (per.FirstName, per.MiddleName, per.LastName, per.Age);
}
然後呼叫端可以使用下列程式碼取用傳回的元組:
var person = GetPersonalInfo("111111111");
Console.WriteLine($"{person.Item1} {person.Item3}: age = {person.Item4}");
在 Tuple 型別定義中也可以將名稱指派給 Tuple 項目。 下例示範使用具名項目的 GetPersonalInfo
方法替代版本:
public (string FName, string MName, string LName, int Age) GetPersonalInfo(string id)
{
PersonInfo per = PersonInfo.RetrieveInfoById(id);
return (per.FirstName, per.MiddleName, per.LastName, per.Age);
}
前一次的 GetPersonalInfo
方法呼叫可修改如下︰
var person = GetPersonalInfo("111111111");
Console.WriteLine($"{person.FName} {person.LName}: age = {person.Age}");
如果方法接受陣列做為參數並修改個別元素的值,方法就不一定要傳回陣列。 C# 會以傳值方式傳遞所有的參考型別,而陣列參考的值是陣列的指標。 在下例中,以 values
方法完成的 DoubleValues
陣列內容變更,都可透過任何具有陣列參考的程式碼觀察到。
public static class ArrayValueExample
{
static void Main()
{
int[] values = [2, 4, 6, 8];
DoubleValues(values);
foreach (var value in values)
{
Console.Write("{0} ", value);
}
}
public static void DoubleValues(int[] arr)
{
for (var ctr = 0; ctr <= arr.GetUpperBound(0); ctr++)
{
arr[ctr] *= 2;
}
}
}
// The example displays the following output:
// 4 8 12 16
一般來說,有兩種方式可將方法加入至現有的型別︰
- 修改該型別的原始程式碼。 如果您同時新增任何私人資料欄位以支援方法,修改來源就會建立中斷性變更。
- 在衍生類別中定義新的方法。 不能以使用其他型別繼承的這種方式新增方法,例如結構和列舉。 也不能用來將方法「新增」至密封的類別。
擴充方法可讓您將方法「新增」至現有的類型,但不必修改型別本身或在繼承的型別中實作新方法。 擴充方法也不必和其擴充型別位於相同的組件中。 呼叫擴充方法就像它是型別的定義成員一樣。
如需詳細資訊,請參閱擴充方法。
使用非同步功能,您就可以呼叫非同步方法,而不需要使用明確回呼或手動將您的程式碼分散到多種方法或 lambda 運算式上。
如果您使用 async 修飾詞來標示方法,可以在方法中使用 await 運算子。 當控制項觸達 async 方法的 await
運算式時,如果等候的工作未完成,控制項會傳回到呼叫端,而有 await
關鍵字的方法中的進度會暫停,直到等候的工作完成。 當工作完成時,方法中的執行可以繼續。
注意
非同步方法會在遇到第一個未完成的等候物件或是到達非同步方法的結尾時 (以先發生者為準),傳回呼叫端。
非同步方法具有的傳回型回,通常是 Task<TResult>、Task、IAsyncEnumerable<T> 或 void
。
void
傳回型別主要用於定義需要 void
傳回型別的事件處理常式。 傳回 void
的非同步方法無法等候,而且 void 傳回方法的呼叫端無法攔截方法擲回的例外狀況。 非同步方法可具備任何類似工作的傳回類型。
在下例中,DelayAsync
是包含會傳回整數之 return 陳述式的非同步方法。 因為它是非同步方法,所以其方法宣告必須有傳回型別 Task<int>
。 因為傳回型別是 Task<int>
,所以 await
中 DoSomethingAsync
運算式的評估會產生整數,如下列 int result = await delayTask
陳述式所示。
class Program
{
static Task Main() => DoSomethingAsync();
static async Task DoSomethingAsync()
{
Task<int> delayTask = DelayAsync();
int result = await delayTask;
// The previous two statements may be combined into
// the following statement.
//int result = await DelayAsync();
Console.WriteLine($"Result: {result}");
}
static async Task<int> DelayAsync()
{
await Task.Delay(100);
return 5;
}
}
// Example output:
// Result: 5
非同步方法不可以宣告任何 in、ref 或 out 參數,但是可以呼叫具有這類參數的方法。
如需非同步方法的詳細資訊,請參閱使用 async 和 await 進行非同步程式設計和非同步傳回型別。
使方法定義立即傳回運算式的結果,或是使具有單一陳述式做為方法的主體很常見。 使用 =>
定義這類方法有個語法捷徑:
public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
public void Print() => Console.WriteLine(First + " " + Last);
// Works with operators, properties, and indexers too.
public static Complex operator +(Complex a, Complex b) => a.Add(b);
public string Name => First + " " + Last;
public Customer this[long id] => store.LookupCustomer(id);
如果方法傳回 void
或為非同步方法,則方法的主體必須是陳述式運算式 (如同 Lambda)。 若為屬性和索引子,它們必須是唯讀,因此您不應使用 get
存取子關鍵字。
迭代器會對集合執行自訂的反覆項目,例如清單或陣列。 迭代器會使用 yield return 陳述式,一次傳回一個項目。 達到 yield return
陳述式時,即會記住目前的位置,讓呼叫端可以要求序列中的下一個項目。
迭代器的傳回型別可以是 IEnumerable、IEnumerable<T>、IAsyncEnumerable<T>、IEnumerator 或 IEnumerator<T>。
如需詳細資訊,請參閱 Iterator。