[轉]C#語法特性總結

本文原地址:C#語法特性總結

作者:louzi

C# 10已與.NET 6、VS2022一起發佈,本文按照.NET的發佈順序,根據微軟官方文檔整理C#中一些有趣的語法特性。

注:基於不同.NET平臺創建的項目,默認支持的C#版本是不一樣的。下面介紹的語法特性,會說明引入C#的版本,在使用過程中,需要注意使用C#的版本是否支持對應的特性。C#語言版本控制,可參考官方文檔

匿名函數

匿名函數是C# 2推出的功能,顧名思義,匿名函數只有方法體,沒有名稱。匿名函數使用delegate創建,可轉換爲委託。匿名函數不需要指定返回值類型,它會根據return語句自動判斷返回值類型。

注:C# 3後推出了lambda表達式,使用lambda可以以更簡潔的方式創建匿名函數,應儘量使用lambda來創建匿名函數。與lambda不同的是,使用delegate創建匿名函數可以省略參數列表,可將其轉換爲具有任何參數列表的委託類型。

// 使用delegate關鍵字創建,無需指定返回值,可轉換爲委託,可省略參數列表(與lambda不同)
Func<int, bool> func = delegate { return true; };

自動屬性

從C# 3開始,當屬性訪問器中不需要其它邏輯時,可以使用自動屬性,以更簡潔的方式聲明屬性。編譯時,編譯器會爲其創建一個僅可以通過get、set訪問器訪問的私有、匿名字段。使用VS開發時,可以通過snippet代碼片段prop+2次tab快速生成自動屬性。

// 屬性老寫法
private string _name;
public string Name
{
    get { return _name; }
    set { _name = value; }
}

// 自動屬性
public string Name { get; set; }

另外,在C# 6以後,可以初始化自動屬性:

public string Name { get; set; } = "Louzi";

匿名類型

匿名類型是C# 3後推出的功能,它無需顯示定義類型,將一組只讀屬性封裝到單個對象中。編譯器會自動推斷匿名類型的每個屬性的類型,並生成類型名稱。從CLR的角度看,匿名類型與其它引用類型沒什麼區別,匿名類型直接派生自object。如果兩個或多個匿名對象指定了順序、名稱、類型相同的屬性,編譯器會把它們視爲相同類型的實例。在創建匿名類型時,如果不指定成員名稱,編譯器會把用於初始化屬性的名稱作爲屬性名稱。

匿名類型多用於LINQ查詢的select查詢表達式。匿名類型使用new與初始化列表創建:

// 使用new與初始化列表創建匿名類型
var person = new { Name = "Louzi", Age = 18 };
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");

// 用於LINQ
var productQuery =
    from prod in products
    select new { prod.Color, prod.Price };

foreach (var v in productQuery)
{
    Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}

LINQ

C# 3推出了殺手鐗功能,查詢表達式,即語言集成查詢(LINQ)。查詢表達式以查詢語法表示查詢,由一組類似SQL的語法編寫的子句組成。

查詢表達式必須以from子句開頭,必須以select或group子句結尾。在第一個from子句與最後一個select或group子句之間,可以包含:where、orderby、join、let、其它from子句等。

可以爲SQL數據庫、XML文檔、ADO.NET數據集及實現了IEnumerable或IEnumerable接口的集合對象進行LINQ查詢。

完整的查詢包括創建數據源、定義查詢表達式、執行查詢。查詢表達式變量是存儲查詢而不是查詢結果,只有在循環訪問查詢變量後,纔會執行查詢。

可使用查詢語法表示的任何查詢都可以使用方法表示,建議使用更易讀的查詢語法。有些查詢操作(如 Count 或 Max)沒有等效的查詢表達式子句,必須使用方法調用。 可以結合使用方法調用和查詢語法。

關於LINQ的詳細文檔,參見微軟官方文檔

// Data source.
int[] scores = { 90, 71, 82, 93, 75, 82 };

// Query Expression.
IEnumerable<int> scoreQuery = //query variable
    from score in scores //required
    where score > 80 // optional
    orderby score descending // optional
    select score; //must end with select or group

// Execute the query to produce the results
foreach (int testScore in scoreQuery)
{
    Console.WriteLine(testScore);
}

Lambda

C# 3推出了很多強大的功能,如自動屬性、擴展方法、隱式類型、LINQ,以及Lambda表達式。

創建Lambda表達式,需要在 => 左側指定輸入參數(空括號指定零個參數,一個參數可以省略括號),右側指定表達式或語句塊(通常兩三條語句)。任何Lambda表達式都可以轉換爲委託類型,表達式Lambda語句還可以轉換爲表達式樹(語句Lambda不可以)。

匿名函數可以省略參數列表,Lambda中不使用的參數可以使用棄元指定(C# 9)。

使用async和await,可以創建包含異步處理的Lambda表達式和語句(C# 5)。

從C# 10開始,當編譯器無法推斷返回類型時,可以在參數前面指定Lambda表達式的返回類型,此時參數必須加括號。

// Lambda轉換爲委託
Func<int, int> square = x => x * x;
// Lambda轉換爲表達式樹
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
// 使用棄元指定不使用的參數
Func<int, int, int> constant = (_, _) => 42;
// 異步Lambda
var lambdaAsync = async () => await JustDelayAsync();
Console.WriteLine($"main thread id: {Thread.CurrentThread.ManagedThreadId}");
lambdaAsync();

static async Task JustDelayAsync()
{
    await Task.Delay(1000);
    Console.WriteLine($"JustDelayAsync thread id: {Thread.CurrentThread.ManagedThreadId}");
}
// 指定返回類型,不指定返回類型會報錯
var choose = object (bool b) => b ? 1 : "two";

擴展方法

擴展方法也是C# 3推出的功能,它能夠向現有類型添加方法,且無需修改原始類型。擴展方法是一種靜態方法,不過是通過實例對象語法進行調用,它的第一個參數指定方法操作的類型,用this修飾。編譯器在編譯爲IL時會轉換爲靜態方法的調用。

如果類型中具有與擴展方法相同名稱和簽名的方法,則編譯器會選擇類型中的方法。編譯器進行方法調用時,會先在該類型的的實例方法中尋找,找不到再去搜索該類型的擴展方法。

最常見的擴展方法是LINQ,它將查詢功能添加到現有的System.Collections.IEnumerable和System.Collections.Generic.IEnumerable類型中。

爲struct添加擴展方法時,由於是值傳遞,只能對struct對象的副本進行更改。從C# 7.2開始,可以爲第一個參數添加ref修飾以進行引用傳遞,這樣就可以對struct對象本身進行修改了。

static class MyExtensions
{
    public static void OutputStringExtension(this string s) => Console.WriteLine($"output: {s}");

    public static void OutputPointExtension(this Point p)
    {
        p.X = 10;
        p.Y = 10;
        Console.WriteLine($"output: ({p.X}, {p.Y})");
    }

    public static void OutputPointWithRefExtension(ref this Point p)
    {
        p.X = 20;
        p.Y = 20;
        Console.WriteLine($"output: ({p.X}, {p.Y})");
    }
}

// class擴展方法
"Louzi".OutputStringExtension();

// struct擴展方法
Point p = new Point(5, 5);
p.OutputPointExtension(); // output: (10, 10)
Console.WriteLine($"original point: ({p.X}, {p.Y})");  // output: (5, 5)
p.OutputPointWithRefExtension();  // output: (20, 20)
Console.WriteLine($"original point: ({p.X}, {p.Y})");  // output: (20, 20)

隱式類型(var)

從C# 3開始,在方法範圍內可以聲明隱式類型變量(var)。隱式類型爲強類型,由編譯器決定類型。

var常用於調用構造函數創建對象實例時,從C# 9開始,這種場景也可以使用確定類型的new表達式:

// 隱式類型
var s = new List<int>();

// new表達式
List<int> ss = new();

注:當返回匿名類型時,只能使用var。

對象、集合初始化列表

從C# 3開始,可以在單條語句中實例化對象或集合並執行成員分配。

使用對象初始化列表,可以在創建對象時向對象的任何可訪問字段或屬性分配值,可以指定構造函數參數或忽略參數以及括號。

public class Person
{
    // 自動屬性
    public int Age { get; set; }
    public string Name { get; set; }

    public Person() { }

    public Person(string name)
    {
        Name = name;
    }
}

var p1 = new Person { Age = 18, Name = "Louzi" };
var p2 = new Person("Sherilyn") { Age = 18 };

從C# 6開始,對象初始化列表不僅可以初始化可訪問字段和屬性,還可以設置索引器。

public class MyIntArray
{
    public int CurrentIndex { get; set; }

    public int[] data = new int[3];

    public int this[int index]
    {
        get => data[index];
        set => data[index] = value;
    }
}

var myArray = new MyIntArray { [0] = 1, [1] = 3, [2] = 5, CurrentIndex = 0 };

集合初始化列表可以指定一個或多個初始值:

var persons = new List<Person>
{
    new Person { Age = 18, Name = "Louzi" },
    new Person { Age = 18, Name = "Sherilyn" }
};

內置泛型委託

.NET Framework 3.5/4.0,分別提供了內置的Action和Func泛型委託類型。void返回類型的委託可以使用Action類型,Action的變體最多有16個參數。有返回值類型的委託可以使用Func類型,Func類型的變體最多同樣16個參數,返回類型爲Func聲明中的最後一個類型參數。

Action<int> actionInstance = ActionInstance;
Func<int, string> funcInstance = FuncInstance;

static void ActionInstance(int n) => Console.WriteLine($"input: {n}");

static string FuncInstance(int n) => $"param: {n}";

dynamic

C# 4主要的功能就是引入了dynamic關鍵字。dynamic類型在變量使用及其成員引用時會繞過編譯時類型檢查,在運行時再進行解析。這便實現了與動態類型語言(如JavaScript)類似的構造。

dynamic dyn = 1;
Console.WriteLine(dyn.GetType()); // output: System.Int32
dyn = dyn + 3; // 如果dyn是object類型,此句則會報錯

命名參數與可選參數

C# 4引入了命名參數和可選參數。命名參數可爲形參指定實參,方式是指定匹配的實參與形參,這時無需匹配參數列表中的位置。可選參數通過指定參數默認值,可以省略實參。可選參數需位於參數列表末尾,如果爲一系列可選參數中的任意一個提供了實參,則必須爲該參數前面的所有可選參數提供實參。

也可以使用OptionalAttribute特性聲明可選參數,此時無需爲形參提供默認值。

// 命名參數與可選參數
PrintPerson(age: 18, name: "Louzi");

// static void PrintPerson(string name, int age, [Optional, DefaultParameterValue("男")] string sex)
static void PrintPerson(string name, int age, string sex = "男") =>
    Console.WriteLine($"name: {name}, age: {age}, sex: {sex}");

靜態導入

C# 6中推出了靜態導入功能,使用using static指令導入類型,可以無需指定類型名稱即可訪問其靜態成員和嵌套類型,這樣避免了重複輸入類型名稱導致的晦澀代碼。

using static System.Console;

WriteLine("Hello CSharp");

異常篩選器(when)

從C# 6開始,when可用於catch語句中,用來指定爲執行特定異常處理程序必須爲true的條件表達式,當表達式爲false時,則不會執行異常處理。

public static async Task<string> MakeRequest()
{
    var client = new HttpClient();
    var streamTask = client.GetStringAsync("https://localHost:10000");
    try
    {
        var responseText = await streamTask;
        return responseText;
    }
    catch (HttpRequestException e) when (e.Message.Contains("301"))
    {
        return "Site Moved";
    }
    catch (HttpRequestException e) when (e.Message.Contains("404"))
    {
        return "Page Not Found";
    }
    catch (HttpRequestException e)
    {
        return e.Message;
    }
}

自動屬性初始化表達式

C# 6開始,可以爲自動屬性指定初始化值以使用類型默認值以外的值:

public class DefaultValueOfProperty
{
    public string MyProperty { get; set; } = "Property";
}

表達式體

從C# 6起,支持方法、運算符和只讀屬性的表達式體定義,自C# 7.0起,支持構造函數、終結器、屬性、索引器的表達式體定義。

static void NewLine() => Console.WriteLine();

null條件運算符

C# 6起,推出了null條件運算符,僅當操作數的計算結果爲非null時,null條件運算符纔會將成員訪問?.或元素訪問?[]運算應用於其操作數;否則,將返回null。

// null條件表達式
public class ConditionalNull
{
    event EventHandler AEvent;

    public void RaiseAEvent() => AEvent?.Invoke(this, EventArgs.Empty);
}

內插字符串

從C# 6開始,可以使用$在字符串中插入表達式,使代碼可讀性更高也降低了字符串拼接出錯的概率。如果在內插字符串中包含大括號,需使用兩個大括號("{{"或""}}")。如果內插表達式需使用條件運算符,需要將其放在括號內。從C# 8起,可以使用\(@"..."或@\)"..."形式的內插逐字字符串,在此之前的版本,必須使用$@"..."形式。

Console.WriteLine($"{name} is {age} year{(age == 1 ? "" : "s")} old.");

nameof

C# 6提供了nameof表達式,nameof可生成變量、類型或成員名稱(非完全限定)作爲字符串常量。

public string Name
{
    get => name;
    set => name = value ?? throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null");
}

out改進

C# 7.0中對out語法進行了改進,可以直接在方法調用的參數列表中聲明out變量,無需再單獨編寫一條聲明語句:

void Function(out int arg) { ... }

// 未改進前
int n;
Function(out n);

// 改進後
Function(out int n);

元組

C# 7.0中引入了對元組的語言支持(之前版本也有元組但效率低下),可以使用元組表示包含多個數據的簡單結構,無需再專門寫一個class或struct。元組是值類型的,是包含多個公共字段以表示數據成員的輕量級數據結構,無法爲其定義方法。C# 7.3後元組支持==與!=。

// 方式一,使用元組字段的默認名稱:Item1、Item2、Item3等
(string, string) unnamedLetters = ("a", "b");
Console.WriteLine($"{unnamedLetters.Item1}, {unnamedLetters.Item2}");
// 方式二
(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");
// 方式三
var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");
// 方式四,C# 7.1開始支持自動推斷變量名稱
int count = 5;
string label = "Colors used in the map";
var pair = (count, label); // 元組元素名爲"count"和"label"

當某方法返回元組時,如需提取元組成員,可通過爲元組的每個值聲明單獨的變量來實現,稱爲解構元組。使用元組作爲方法返回類型,可以替代定義out方法參數。

// 解構元組
var (first, last) = Range(numbers);
Console.WriteLine($"{first} to {last}");

(int max, int min) = Range(numbers);
Console.WriteLine($"{min} to {max}");

棄元

從C# 7.0開始支持棄元,棄元是佔位符變量,相當於未賦值的變量,表示不想使用該變量,使用下劃線_表示棄元變量。如下列舉了一些棄元的使用場景:

// 場景一:丟棄元組值
(_, _, area) = city.GetCityInformation(cityName);

// 場景二:從C# 9開始,可以丟棄Lambda表達式中的參數
Func<int, int, int> constant = (_, _) => 42;

// 場景三,丟棄out參數
DiscardsOut(out _);
static void DiscardsOut(out string s)
{
    s = "nothing";
    Console.WriteLine($"input is {s}");
}

模式匹配

C# 7.0添加了模式匹配功能,之後每個主要C#版本都擴展了模式匹配功能。模式匹配用來測試表達式是否具有某些特徵,is表達式、switch語句和switch表達式均支持模式匹配,可使用when關鍵字來指定模式的其他規則。

模式匹配目前包含這些類型:聲明模式、類型模式、常量模式、關係模式、邏輯模式、屬性模式、位置模式、var模式、棄元模式,詳細內容可參考官方文檔。

is模式表達式改進了is運算符功能,可在一條指令分配結果:

// is模式匹配
if (input is int count) do somthing... ;

list.Where(w is { id: 24, Deleted: false or null }).ToList();

// 老寫法
if (input is int)
{
    int count = (int)input;
    do somthing... ;
}

// is模式進行空檢查
string? message = "This is not the null string";
if (message is not null) Console.WriteLine(message);

default文本表達式

默認值表達式生成類型的默認值,之前版本僅支持default運算符,C# 7.1後增強了default表達式的功能,當編譯器可以推斷表達式類型時,可以使用default生成類型的默認值。

// 新寫法
Func<string, bool> whereClause = default;
// 老寫法
Func<string, bool> whereClause = default(Func<string, bool>);

switch表達式

從C# 8開始,可以使用switch表達式。switch表達式相較於switch語句的改進之處在於:

  • 變量在switch關鍵字之前;
  • 使用=>替換case :結構;
  • 使用棄元_替換default運算符;
  • 使用表達式替換語句。
public enum Level
{
    One,
    Two,
    Three
}
public static int LevelToScore(Level level) => level switch
{
    Level.One   => 1,
    Level.Two   => 5,
    Level.Three => 10,
    _ => throw new ArgumentOutOfRangeException(nameof(level), $"Not expected level value: {level}"),
};

using聲明

C# 8添加了using聲明功能,它指示編譯器聲明的變量應在代碼塊的末尾進行處理。 using聲明相比傳統的using語句代碼更簡潔,這兩種寫法都會使編譯器在代碼塊末尾調用Dispose()。

static void WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines.txt");
    do somthing... ;
    return;
    // file is disposed here
}

索引和範圍

C# 8中添加了索引和範圍功能,爲訪問序列中的單個元素或範圍提供了簡潔的語法。該語法依賴兩個新類型與兩個新運算符:

  • System.Index表示一個序列索引;
  • System.Range表示序列的子範圍;
  • 末尾運算符^,使用該運算符加數字,指定倒數第幾個;
  • 範圍運算符..,指定範圍的開始和末尾。

範圍運算符包括此範圍的開始,但不包括此範圍的末尾。

var words = new string[]
{               // 正常索引             索引對應的末尾運算符
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumped",   // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
};              // 9 (words.Length)    ^0

Console.WriteLine($"The last word is {words[^1]}"); // dog
var allWords = words[..]; // 包含所有值,等同於words[0..^0].
var firstPhrase = words[..4]; // 開始到words[4],不包含words[4]
var lastPhrase = words[6..]; // words[6]到末尾
// 聲明範圍變量
Range phrase = 1..4;
var text = words[phrase];

??與??=

??合併運算符:C# 6後可用,如果左操作數的值不爲null,則??返回該值;否則,它會計算右操作數並返回其結果。如果左操作數的計算結果爲非null,則不會計算其右操作數。

??=合併賦值運算符:C# 8後可用,僅在左側操作數的求值結果爲null時,纔將右操作數的值賦值給左操作數。否則,不會計算其右操作數。??=運算符的左操作數必須是變量、屬性或索引器元素。

// ??合併運算符
Console.WriteLine($"name is {OutputName(null)}");
static string OutputName(string name) => name ?? "some one";

// 使用??=賦值運算符
variable ??= expression;

// 老寫法
if (variable is null)
{
    variable = expression;
}

頂級語句

C# 9推出了頂級語句,它從應用程序中刪除了不必要的流程,應用程序中只有一個文件可使用頂級語句。頂級語句使主程序更易讀,減少了不必要的模式:命名空間、class Program和static void Main()。

使用VS創建命令行項目,選擇.NET 5及以上版本,就會使用頂級語句。

// 使用VS2022創建.NET 6.0平臺的命令行程序默認生成的內容
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

global using

C# 10添加了global using指令,當關鍵字global出現在using指令之前時,該using適用於整個項目,這樣可以減少每個文件using指令的行數。global using 指令可以出現在任何源代碼文件的開頭,但需添加在非全局using之前。

global修飾符可以與static修飾符一起使用,也可以應用於using別名指令。在這兩種情況下,指令的作用域都是當前編譯中的所有文件。

global using System;
global using static System.Console; // 全局靜態導入
global using Env = System.Environment; // 全局別名

文件範圍的命名空間

C# 10引入了文件範圍的命名空間,可將命名空間包含爲語句,後加分號且無需添加大括號。一個代碼文件通常只包含一個命名空間,這樣簡化了代碼且消除了一層嵌套。文件範圍的命名空間不能聲明嵌套的命名空間或第二個文件範圍的命名空間,且它必須在聲明任何類型之前,該文件內的所有類型都屬於該命名空間。

using System;

namespace SampleFileScopedNamespace;

class SampleClass { }

interface ISampleInterface { }

struct SampleStruct { }

enum SampleEnum { a, b }

delegate void SampleDelegate(int i);

with表達式

C# 9開始引入了with表達式,它使用修改的特定屬性和字段生成其操作對象的副本,未修改的值將保留與原對象相同的值。對於引用類型成員,在複製操作數時僅複製對該成員實例的引用,with表達式生成的副本和原對象都具有對同一引用類型實例的訪問權限。

在C# 9中,with表達式的左操作數必須爲record類型,C# 10進行了改進,with表達式的左操作數也可以是struct類型。

public record NamedPoint(string Name, int X, int Y);

var p1 = new NamedPoint("A", 0, 0);
var p2 = p1 with { Name = "B", X = 5 };

原始字符串文本

原始字符串字面量是字符串字面量的一種新格式。 原始字符串字面量可以包含任意文本,包括空格、新行、嵌入引號和其他特殊字符,無需轉義序列。 原始字符串字面量以至少三個雙引號 (""") 字符開頭。 它以相同數量的雙引號字符結尾。 通常,原始字符串字面量在單個行上使用三個雙引號來開始字符串,在另一行上用三個雙引號來結束字符串。 左引號之後、右引號之前的換行符不包括在最終內容中:

string longMessage = """
    This is a long message.
    It has several lines.
        Some are indented
                more than others.
    Some should start at the first column.
    Some have "quoted text" in them.
    """;

右雙引號左側的任何空格都將從字符串字面量中刪除。 原始字符串字面量可以與字符串內插結合使用,以在輸出文本中包含大括號。 多個 $ 字符表示有多少個連續的大括號開始和結束內插:

var location = $$"""
   You are at {{{Longitude}}, {{Latitude}}}
   """;

非記錄類和結構的主構造函數

主構造函數允許將參數添加到類聲明本身,並在類主體中使用這些值。
作爲記錄位置語法的一部分,主構造函數在 C# 9 中引入,而 C# 12 則將主構造函數擴展到所有類和結構。
主構造函數的基本語法和用法:

public class Student(int id, string name, IEnumerable<decimal> grades)
{
    public Student(int id, string name) : this(id, name, Enumerable.Empty<decimal>()) { }
    public int Id => id;
    public string Name { get; set; } = name.Trim();
    public decimal GPA => grades.Any() ? grades.Average() : 4.0m;
} 

上面 Student 類中的主構造函數參數在類的整個主體中都可用。

所有類型均可使用 using 指令起別名

C# 12 將 using 指令支持擴展到任何類型,如:

using Measurement = (string, int);
using PathOfPoints = int[];
using DatabaseInt = int?;

可爲幾乎任何類型起別,如可空值類型,但不能爲可空引用類型起別名。

元組比較特別,它可以包含元素名稱和類型:

using Measurement = (string Units, int Distance);

可以在任何需要使用類型的地方使用別名。例如:

public void F(Measurement x)
{ }

lambda 表達式的默認值

C# 12 通過允許爲參數指定默認值,進一步增強了 lambda 表達式的能力。
語法與其他默認參數相同:

var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6

與其他默認值類似,lambda 的默認值將在元數據中發出,並可通過反射作爲 lambda Method 屬性的 ParameterInfo 的 DefaultValue 獲得。例如:

var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault.Method.GetParameters()[0].DefaultValue; // 2

在 C# 12 之前,需要使用本地函數或 System.Runtime.InteropServices 命名空間中的 DefaultParameterValue 來爲 lambda 表達式參數提供默認值。這些方法仍然有效,但難閱讀,且與方法的默認值不一致。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章