C# 中的方法

方法是包含一系列陳述式的程式碼區塊。 程式會造成呼叫方法並指定任何所需的方法引數來執行陳述式。 在 C# 中,每個執行的指示是在方法的內容中執行。 Main 方法是每個 C# 應用程式的進入點,而且它是由通用語言執行平台 (CLR) 啟動程式時呼叫。

注意

本主題討論具名的方法。 如需匿名函式的資訊,請參閱 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 方法包含名為 iint 型別的單一參數。 第一個方法呼叫會傳遞給 Square 方法型別 intnum 變數,第二個傳遞數值常數,第三個傳遞運算式。

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 欄位的修改,也會將 Main 方法中引數 rtvalue 欄位變更成 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 = {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 關鍵字標記的參數必須是陣列型別,而且必須是方法參數清單中的最後一個參數。

然後呼叫端會以下列四種方式之一叫用方法︰

  • 傳遞包含所需項目數目的適當型別陣列。
  • 將適當型別各個引數的逗點分隔清單傳遞給方法。
  • 藉由傳遞 null
  • 不提供引數給參數陣列。

下列範例會定義能從參數陣列中傳回所有母音,名為 GetVowels 的方法。 Main 方法會示範這四種叫用方法的方法。 呼叫端不需要針對包含 params 修飾詞的參數提供任何引數。 在此情況下,參數會是空陣列。

static class ParamsExample
{
    static void Main()
    {
        string fromArray = GetVowels(["apple", "banana", "pear"]);
        Console.WriteLine($"Vowels from array: '{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 string[]? input)
    {
        if (input == null || input.Length == 0)
        {
            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: ''

選擇性參數和引數

方法定義可以指定其參數為必要項目或選擇項目。 根據預設,參數為必要項目。 在方法定義中包含參數的預設值即可指定選擇性參數。 呼叫此方法時,如不為選擇性參數提供任何引數,則改用預設值。

參數的預設值必須由下列運算式種類之一指派︰

  • 常數,例如常值字串或數字。

  • 格式 default(SomeType) 的運算式,其中 SomeType 可以是實值型別或參考型別。 如果是參考型別,則實際上與指定 null 相同。 您可以使用 default 常值,因為編譯器可以從參數的宣告推斷類型。

  • new ValType() 形式的運算式,其中 ValType 是實值型別。 這會叫用實值型別的隱含無參數建構函式,它不是該型別的實際成員。

    注意

    在 C# 10 和更新版本中,當表單 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;
}

若要使用從方法傳回的值,呼叫方法可以在使用相同類型值的任意位置使用方法呼叫本身即已足夠。 您也可以指派傳回值給變數。 例如,下列兩個程式碼範例會達到相同的目標:

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來儲存值是選擇性的。 它有助於程式碼的可讀性,或如果您需要儲存方法的整個範圍引數的原始值,則可能為必要。

有時候,您希望自己的方法傳回的不止單一值。 您可以使用「Tuple 型別」和「Tuple 常值」輕鬆完成。 Tuple 型別會定義 Tuple 項目的資料類型。 Tuple 常值會提供傳回 Tuple 的實際值。 在下列範例中,(string, string, string, int) 會定義由 GetPersonalInfo 方法所傳回的 Tuple 類型。 運算式 (per.FirstName, per.MiddleName, per.LastName, per.Age) 是 Tuple 常值,方法會傳回 PersonInfo 物件的名字、中間名和姓氏以及年齡。

public (string, string, string, int) GetPersonalInfo(string id)
{
    PersonInfo per = PersonInfo.RetrieveInfoById(id);
    return (per.FirstName, per.MiddleName, per.LastName, per.Age);
}

然後呼叫端使用傳回的 Tuple 及程式碼,如下所示︰

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# 會以傳值方式傳遞所有的參考型別,而陣列參考的值是陣列的指標。 在下例中,以 DoubleValues 方法完成的 values 陣列內容變更,都可透過任何具有陣列參考的程式碼觀察到。


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>,所以 DoSomethingAsyncawait 運算式的評估會產生整數,如下列 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

另請參閱