共用方式為


C# 中的方法

方法是包含一系列陳述式的程式碼區塊。 程序會透過呼叫方法並指定任何所需的參數以執行陳述式。 在 C# 中,每個執行的指示是在方法的內容中執行。

注意

本文討論具名方法。 如需匿名函式的資訊,請參閱 Lambda 運算式

方法簽名

classrecordstruct 中指定以下項目以宣告方法:

  • 選擇性的存取層級,例如 publicprivate。 預設值為 private
  • 選擇性修飾詞,例如 abstractsealed
  • 傳回值,或如果方法沒有傳回值,則為 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 傳遞給 方法;第二個傳遞數值常數;第三個傳遞運算式。

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 {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(speed: 60, miles: 170);
        Console.WriteLine($"Travel time: approx. {travelTime} hours");
    }
}
// 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: {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: {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 方法中引數 rtMain 欄位變更成 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;
}

以傳址方式傳遞參數

當您想要變更方法中的參數值,並希望此變更在控制返回到呼叫方法時被反映,您應以參考傳遞的方式傳遞參數。 若要以傳址方式傳遞參數,請使用 refout 關鍵字。 您也可以用傳址方式傳遞值,預防複製,同時使用 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 = {i}  j = {j}");

        Swap(ref i, ref j);

        Console.WriteLine($"i = {i}  j = {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 是實值型別。 此運算式會叫用實值型別的隱含無參數建構函式,它不是該型別的實際成員。

    注意

    當表單的表示式 new ValType() 叫用實值類型的明確定義無參數建構函式時,編譯程式會產生錯誤,因為預設參數值必須是編譯時間常數。 使用 default(ValType) 表達式或 default 常值提供預設參數值。 如需無參數建構函式的詳細資訊,請參閱結構類型一文中的結構初始化和預設值一節。

如果方法同時包含必要和選擇性參數,則選擇性參數會定義在參數清單結尾,在所有必要參數的後面。

下例會定義 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 SimpleMathExtension
{
    public int DivideTwoNumbers(int number1, int number2)
    {
        return number1 / number2;
    }
}

若要使用從方法傳回的值,您可以將傳回值指派給變數:

int result = obj.DivideTwoNumbers(6,2);
// The result is 3.
Console.WriteLine(result);

呼叫方法也可以使用 方法呼叫本身,只要相同類型的值就已足夠。 例如,下列兩個程式代碼範例會達成相同的目標:

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);

有時候,您希望方法傳回的不止單一值。 您可以使用「元組型別」和「元組常值」來傳回多個值。 元組型別會定義元組元素的資料類型。 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}");

在元組型別定義中也可以將名稱指派給元組元素。 下例示範使用具名項目的 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>TaskIAsyncEnumerable<T>voidvoid 傳回型別主要用於定義需要 void 傳回型別的事件處理常式。 傳回 void 的非同步方法無法等候,而回傳類型為 void 的方法呼叫端無法攔截方法擲回的例外狀況。 非同步方法可具備任何類似工作的傳回類型

在下例中,DelayAsync 是包含會傳回整數之 return 陳述式的非同步方法。 因為它是非同步方法,所以其方法宣告必須有傳回型別 Task<int>。 因為傳回型別是 Task<int>,所以 awaitDoSomethingAsync 運算式的評估會產生整數,如下列 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

非同步方法不可以宣告任何 inrefout 參數,但是可以呼叫具有這類參數的方法。

如需非同步方法的詳細資訊,請參閱使用 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 陳述式時,即會記住目前的位置,讓呼叫端可以要求序列中的下一個項目。

迭代器的傳回型別可以是 IEnumerableIEnumerable<T>IAsyncEnumerable<T>IEnumeratorIEnumerator<T>

如需詳細資訊,請參閱 Iterator

另請參閱