C#基礎語法

颜骏發表於2024-11-13

C#

程式設計規範

命名規則

  1. 程式設計符必須以字母或下劃線開頭
  2. 識別符號可以包含Unicode字元、十進位制數字字元、Unicode連線字元、Unicode組合字元或Unicode格式字元

可以在識別符號上使用@字首宣告與C#關鍵字匹配的識別符號。eg.@if宣告為if的識別符號

命名約定

型別名稱、名稱空間和所有公共成員使用PascalCase

  • 介面名稱以I開頭,屬性型別以Attribute結尾,對變數、方法和類使用有意義的描述名稱
  • 列舉型別對非標記使用單數名詞,對標記使用複數名詞,識別符號不應包含兩個連續的下劃線_字元,這些名稱保留給編譯器生成
  • 清晰>簡潔,類名和方法名稱採用PascalCase,常量名包括欄位和區域性變數也是
  • 對方法引數、區域性變數使用駝峰式大小寫,專用例項欄位以下劃線_開頭,其餘文字為駝峰式大小寫,靜態欄位以s_開頭
  • 避免在名稱中使用縮寫或首字母,廣為人知的除外,使用遵循反向域名錶示法的名稱空間
  • S用於結構,C用於類,M用於方法,v用於變數,p用於引數,r用於ref引數

編碼約定

字串

  1. 使用內插連線短字串
string displayName = $"{first}, {second}";
  1. 迴圈追加字串,尤其是大量文字,使用stringbuilder
var a = "aaaaaaaaaaaaaaaaaaa";
var sb = new StringBuilder();
foreach (var i in a)
{
    sb.Append(i);
}

陣列

  1. 宣告行上初始化陣列時,使用簡介的語法
// 不能使用var
int[] v1 = {1, 2, 3, 4, 5};
// 顯式
var v2 = new string[]{"a, b, c, d"};

委託

使用Func<>和Action<>,而不是委託在類中定義委託方法

Action<string> a1 = x => Console.WriteLine($"x is:{x}");
Action<string, string> a2 = (x, y) => Console.WriteLine($"x is:{x}, y is {y}");

Func<string, int> f1 = x => Convert.ToInt32(x);
Func<int, int, int> f2 = (x, y) => x + y;

// 使用Func<> 或 Action<>委託定義的簽名呼叫方法
a1("string for x");
a2("string for x", "string for y");
Console.WriteLine($"The value is {f1("1")}");
Console.WriteLine($"The sum is {f2(1, 2)}");

多播委託

// 委託是一種宣告,類似於一個介面,帶有關鍵字,後面跟著返回型別和引數列表
public delegate void MyDelegate(string message);

public class Program
{
    public static void Main()
    {
        MyDelegate printDelegate = new MyDelegate(PrintMessage);
        printDelegate += PrintUpperCase;
        printDelegate("Hello World");
    }
    public static void PrintMessage(string message)
    {
        Console.WriteLine(message);
    }
    public static void PrintUpperCase(string message)
    {
        Console.WriteLine(message.ToUpper());
    }
}

釋放資源

// 顯示釋放資源
Font bodyStyle = new Font("Arial", 10.0f);
try
{
    byte charset = bodyStyle.GdiCharSet;
}
finally
{
    if (bodyStyle != null)
    {
        ((IDisposable)bodyStyle).Dispose();
    }
}
// 使用using,在離開作用域時自動呼叫Dispose方法,即使出現異常任然可以釋放掉資源
using Font normalStyle = new Font("Arial", 10.0f);
byte charset3 = normalStyle.GdiCharSet;

new關鍵字

// 建立物件時下列兩種方法等效
var user = new User();
User user2 = new();

索引

^類似於取反,..類似於python中的:,可以切片。StringSpanReadOnlySpanList 支援索引,但不支援範圍。

在陣列中獲取範圍是從初始陣列複製的陣列而不是引用的陣列,修改生成的值不會更改陣列中的值

string[] words = [
    // index from start    index from end
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
];
// brown
Console.WriteLine(words[^1]);
// uick
Console.WriteLine(words[1][1..]);
// ui
Console.WriteLine(words[1][1..3]);
// brown
Console.WriteLine(words[words.Length - 1]);

Base

類比理解為Java中的extend和implement,具有幾個不同的用途,都與繼承有關

// 呼叫基類建構函式
public class DerivedClass: BaseClass{
    public DerivedClass(): base(){}		// 呼叫基類建構函式
    public DerivedClass(): base(value){}	// 呼叫且傳參
}

// 訪問基類成員
public class BaseClass{
    public void PrintMessage() {
        CW("...");
    }
}

public class Derived: BaseClass{
    public void PrintMessage() {
        CW("...");
        base.PrintMessage();	// 呼叫基類PrintMessage方法
    }
}

// 指定基類作為強制轉換目標
public class BaseClass{}

public class Derived: BaseClass{
    public void Method() {
        BaseClass bc = base;	// 將派生類物件轉換為基類型別
    }
}

?.條件運算子

類似Java裡面的optional,左側為null則表示式為null,不為null則走後續流程,避免空指標

if pwd == auth?.pwd

??合併運算子

類似Python的海象符,如果左邊為null則返回右邊的數

string result = str ?? "default";

Using()

Using是資源管理語句,常用於需要顯式釋放的非託管資源,如檔案流、網路連線、資料庫連線等。

  • using 塊內的程式碼執行完畢時,無論是正常完成還是因為異常而退出,using 語句都會自動呼叫每個物件的 Dispose 方法。這樣可以確保釋放
// base64編碼的字串轉換成位元組陣列
using(var mStream = new MemoryStream(Convert.FromBase64String(source)));

// 使用解密器對資料解密
using (var cryptoStream = new CryptoStream(mStream,
                       DesHandler.CreateDecryptor(DesHandler.Key, DesHandler.IV), CryptoStreamMode.Read));

// 建立物件讀取解密後的文字資料
using (var reader = new StreamReader(cryptoStream));

readonly

類比Java中的finall,初始化後不能修改

  • 不能用於修飾靜態欄位、修飾類或介面
  • 欄位或區域性變數使用使用了readonly則不需要const
// 欄位,必須在宣告時或建構函式中初始化,不能修改
public readonly int Field;

// 變數,必須在宣告時初始化,不能修改
public void Func() {
    readonly int local = 10;
}

// 方法,不能包含任何可變引數,不可丟擲異常
public readonly void MyMethod() {
    // pass
}

// 委託,不能更改
public readonly Action MyDelegate;

// 屬性,get訪問器必須返回一個常量,而set訪問器不能被實現
public readonly int Property{get; private set;}

out關鍵字

用於宣告的一個輸出引數,方法呼叫時透過引數透過引用傳遞,如果使用out引數則必須在方法體內為每個out引數賦值。我的理解是,就是將帶有out關鍵字的引數return出去,

public void Example(int a, out int b) {
    b = a * 2;
}
int result;
Example(5, out result);	// result = 10

匿名型別

將只讀資料封裝到單個物件中,而無需顯示定義一個型別

var v = new {Amount = 108, Message = 'Hello'};

通常用在查詢表示式的select子句中(LINQ),其用來初始化的屬性不能為null、匿名函式或指標型別。匿名型別是class型別,直接派生自object,且無法強制轉換為除object外的任何型別,支援採用with表示式形式的非破壞性修改,類似於Java中的Set不過只能修改已存在的屬性。匿名型別會重寫ToString方法

var apple = new {Item = "apple", Price = 1.35};
var onSale = apple with {Price=0.79};
Console.WriteLine(apple);
Console.WriteLine(onSale);

record 記錄關鍵字

類似Java中的final,預設實現不可變性(不可更改物件的任何屬性或欄位值)、Equals和GetHashCode方法及ToString方法,自動提供了建構函式、屬性的比較和字串表示的功能

  • 值相等性:record 型別自動擁有一個經過最佳化的 Equals 方法,該方法比較兩個 record 例項的欄位值是否相等。
  • 簡潔的宣告式語法:record 允許使用更簡潔的語法來宣告只包含資料的類。
  • 不可變性:record 型別的例項一旦建立,其狀態就不能更改(除非顯式地使用可變記錄)。
  • 繼承:record 可以繼承自其他 record 或 class,並且可以新增額外的成員。

以下情況考慮使用記錄:

  1. 定義依賴值相等性的資料模型
  2. 定義物件不可變型別

關係模式

string WaterState(int temp) => temp switch
    {
        (>32) and (<212) => "liquid",
        < 32 => "solid",
        > 212 => "gas",
        32 => "solid/liquid transition",
        212 => "liquid/gas transition",
        _ => throw new ArgumentOutOfRangeException()
    };

判空

str

string.IsNullOrEmpty(s);	// 如果是空串判不出來
string.IsNullOrWhiteSpace(s);	// 空串判的出來

List

if (myList == null || !myList.Any()){}	// 集合為null或沒有元素
if (myList?.Any() == false){}	// 集合為null或沒有元素

物件

if (obj == null) {}	// 物件為空
if (obj is null) {} // 物件為空

Object.ReferenceEquals(obj1, obj2);	// 判斷物件引用地址是否一樣
object.Equals(str1, str2);	// 判斷兩物件值是否相等,需要重寫Equals方法
object DefaultObj = obj ?? new object();	// 使用??運算子提供預設值

值引用

int? myInt = null;
if (!myInt.HasValue) {}
int defaultInt = myInt ?? 0;	// 使用??運算子提供預設值

主建構函式

  1. 初始化屬性
// 只讀情況下
public readonly struct Distance(double dx, double dy)
{
    public readonly double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
    public readonly double Direction { get; } = Math.Atan2(dy, dx);
}

// 兩種方法初始化物件效果一致
public readonly struct Distance
{
    public readonly double Magnitude { get; }

    public readonly double Direction { get; }

    public Distance(double dx, double dy)
    {
        Magnitude = Math.Sqrt(dx * dx + dy * dy);
        Direction = Math.Atan2(dy, dx);
    }
}

// 非只讀情況下,
public struct Distance(double dx, double dy)
{
    public readonly double Magnitude => Math.Sqrt(dx * dx + dy * dy);
    public readonly double Direction => Math.Atan2(dy, dx);

    public void Translate(double deltaX, double deltaY)
    {
        dx += deltaX;
        dy += deltaY;
    }

    public Distance() : this(0,0) { }
}

LINQ

Where

從資料來源中篩選出元素

from city in cities where city.Pop is < 200 and >100 select city;

排序

OrderBy

可按升序或降序排列,例子中以Area為主,population為輔

var orderedEnumerable = from country in countries 
    orderby country.Area, country.Population descending select country;

ThenBy

按升序執行次要排序

Reverse

反轉集合中的元素

Join

將資料來源中元素於另一個資料來源元素進行關聯和/或合併,連線序列之後必須使用select或group語句指定儲存在輸入序列中的元素。示例關聯Category屬性與categories字串陣列中一個類別匹配的prod物件

var cateQuery = from cat in categories
            join prod in products on cat equals prod.Category
            select new
            {
                Category = cat,
                Name = prod.Name
            };

Let

使用let將結果儲存在新範圍變數中

from name in names select names 
    let firstName = name.Split(" ")[0]
    select firstName;

多查詢

var query = from student in students
            // 按照student.Year分組
            group student by student.Year
            // 為分組定義別名,後續使用別名進行分組
            into studentGroup
            select new
            {   // 每個分組的鍵,學生的年級
                Level = studentGroup.Key,
                // 每個分組中,所有學生的平均成績
                HighestScore = (from student2 in studentGroup select student2.ExamScores.Average()).Max()
            };

查詢物件

var entity = from o in InComingOrders
    where o.OrderSize > 5
    select new Customer { Name = o.Name, Phone = o.Phone };
// LINQ寫法
var entity2 = InComingOrders.Where(e => e.OrderSize > 5)
    .Select(e => new Customer { Name = e.Name, Phone = e.Phone });

作為資料表示式(Lambda)

結合out關鍵字返回查詢

void QueryMethod(int[] ints, out List<string> returnQ) =>
            returnQ = (from i in ints where i < 4 select i.ToString()).ToList();

int[] nums = [0, 1, 2, 3, 4, 5, 6, 7];
QueryMethod(nums, out List<string> result);
foreach (var item in result)
{
    Console.WriteLine(item);
}

eg

// 普通方法編寫查詢總分資料
var studentQuery1 = from student in studnets
    let totalScore = student.Scores[0] + student.Scores[1] + student.Scores[2] + student.Scores[3]
    select totalScore;
// 使用Linq方法編寫查詢總分資料
var studentQuery2 = studnets.Select(e => e.Scores[0] + e.Scores[1] + e.Scores[2] + e.Scores[3]);
// 統計平均分
double average = studentQuery1.Average();

// 將大於平均分的學生資料對映為物件
var query1 =
    from student in studnets
    let x = student.Scores[0] + student.Scores[1] +
            student.Scores[2] + student.Scores[3]
    where x > average
    select new { id = student.ID, score = x };
// 使用Linq寫法
var query2 = studnets.Where(e => e.Scores[0] + e.Scores[1] + e.Scores[2] + e.Scores[3] > average).Select(e =>
    new { id = e.ID, score = e.Scores[0] + e.Scores[1] + e.Scores[2] + e.Scores[3] });
// Linq簡潔寫法
var query3 = studnets.Select(e => new { id = e.ID, score = e.Scores[0] + e.Scores[1] + e.Scores[2] + e.Scores[3] })
    .Where(e => e.score > average);


foreach (var item in query1)
{
    Console.WriteLine("Student ID: {0},Score: {1}", item.id, item.score);
}

投影運算

SelectMany

多個from子句投影字串列表中每個字串中的單詞

List<string> phrases = ["an apple a day", "the quick brown fox"];
// 普通寫法
var query = from phrase in phrases from word in phrase.Split(' ') select word;
// Linq寫法
var query2 = phrases.SelectMany(e => e.Split(' '));

Zip列表壓縮元組,類似python

// An int array with 7 elements.
IEnumerable<int> numbers = [1, 2, 3, 4, 5, 6, 7];
// A char array with 6 elements.
IEnumerable<char> letters = ['A', 'B', 'C', 'D', 'E', 'F'];


foreach (var (first, second, third) in numbers.Zip(letters, emoji))
{
    Console.WriteLine($"Number:{first} is zipped with letter: {second} and emoji {third}");
}

Set集合操作

去重

string[] words = ["the", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog"];
// 去重
var query = from word in words.Distinct() select word;
// 根據條件去重
var query2 = from word in words.DistinctBy(e => e.Length)select word;

差集

string[] words1 = ["the", "quick", "brown", "fox"];
string[] words2 = ["jumped", "over", "the", "lazy", "dog"];
// console:queik、brown、fox,輸出1在2中沒有的元素
IEnumerable<string> query = from word in words1.Except(words2) select word;
// expectBy同理,根據自定義欄位進行操作
var result = new List<Person> { new Person { Name = "Alice" }, new Person { Name = "Bob" } }
                            .ExceptBy(person => person.Name,
                                     new List<Person> { new Person { Name = "Alice" }, new Person { Name = "Charlie" } });
// result 將包含 { new Person { Name = "Bob" } },因為 "Alice" 在兩個集合中都存在,而 "Bob" 和 "Charlie" 只在第一個集合中。

交集

string[] words1 = ["the", "quick", "brown", "fox"];
string[] words2 = ["jumped", "over", "the", "lazy", "dog"];
// 輸出the
IEnumerable<string> query = from word in words1.Intersect(words2) select word;
var list = words1.Intersect(words2).Select(e => e.ToUpper()).ToList();
// 透過比較名稱生成 Teacher 和 Student 的交集
(Student person in
    students.IntersectBy(
        teachers.Select(t => (t.First, t.Last)), s => (s.FirstName, s.LastName)))

並集

string[] words1 = ["the", "quick", "brown", "fox"];
string[] words2 = ["jumped", "over", "the", "lazy", "dog"];
// 使用UnionBy
(var person in
    students.Select(s => (s.FirstName, s.LastName)).UnionBy(
        teachers.Select(t => (FirstName: t.First, LastName: t.Last)), s => (s.FirstName, s.LastName)))
// 輸出:the quick brown fox jumped over lazy dog
var query = (from word in words1.Union(words2) select word).ToList();
var list = words1.Union(words2).ToList();

限定符

  • All():所有
  • Any():任何
  • Contains():正好
IEnumerable<string> names = from student in students
                            where student.Scores.Contains(95)
                            select $"{student.FirstName} {student.LastName}: {string.Join(", ", student.Scores.Select(s => s.ToString()))}";
  • Skip():跳過序列中指定位置之前的元素
  • SkipWhile():基於謂詞函式跳過元素,直到元素不符合條件
  • Take():獲取序列中指定位置之前的元素
  • TakeWhile():同上操作
  • Chunk():將序列元素拆分為指定最大大小的區塊
var resource = Enumerable.Range(0, 8);
// 012
foreach (var i in resource.Take(3)){ }
// 345678
foreach (var i in resource.Skip(3)){ }
// 012345
foreach (var i in resource.TakeWhile(e=>e<5)){ }
// 678
foreach (var i in resource.SkipWhile(e=>e<5)){ }
// 平均分塊,將資料分成三塊,123、456、78
int chunkNum = 1;
foreach (int[] chunk in Enumerable.Range(0, 8).Chunk(3))
{
    Console.WriteLine($"Chunk {chunkNum++}:)");
    foreach (int item in chunk)
    {
        Console.WriteLine($"   {item}");
    }
    Console.WriteLine();
}

資料型別轉換

  1. AsEnumerable - 返回型別轉化為IEnumerable的輸入
  2. AsQueryable - 泛型IEnumerable轉換為泛型IQueryable
  3. Cast - 集合中的元素轉換為指定型別
  4. OfType - 轉換為指定型別的能力篩選值
  5. ToArray - 集合轉換為陣列(強制執行查詢)
  6. ToDictionary - 根據鍵選擇器函式將元素放入 Dictionary。 此方法強制執行查詢
  7. ToList - 集合轉換為List
  8. ToLookUp - 根據鍵選擇器函式將元素放入 Lookup(一對多字典,強制執行查詢

連表

  1. Join - 根據鍵選擇函式Join兩個序列並提取對 - join...in...on...equals
  2. GroupJoin - 根據鍵選擇器函式Join兩個序列,並對每個元素的結果匹配項分組 - join...in...on...equals...into...

單鍵

Teacher和Department匹配,TeacherId與該Teacher相匹配

var query = from department in departments
            join teacher in teachers on department.TeacherID equals teacher.ID
            select new
            {
                DepartmentName = department.Name,
                TeacherName = $"{teacher.First} {teacher.Last}"
            };
// Linq
var query = teachers
    // 主表連線副表,parameter2、3是查詢條件
    .Join(departments, teacher => teacher.ID, department => department.TeacherID,
          // lambda表示式,定義連線結果,建立匿名型別物件
        (teacher, department) =>new { DepartmentName = department.Name, TeacherName = $"{teacher.First} {teacher.Last}" });

組合鍵

IEnumerable<string> query =
    from teacher in teachers
    join student in students on new
    {
        FirstName = teacher.First,
        LastName = teacher.Last
    } equals new
    {
        student.FirstName,
        student.LastName
    }
    select teacher.First + " " + teacher.Last;

// Linq寫法
IEnumerable<string> query = teachers
    .Join(students,
        teacher => new { FirstName = teacher.First, LastName = teacher.Last },
        student => new { student.FirstName, student.LastName },
        (teacher, student) => $"{teacher.First} {teacher.Last}"
 );

多聯結

var query = from student in students
    join department in departments on student.DepartmentID equals department.ID
    join teacher in teachers on department.TeacherID equals teacher.ID
    select new {
        StudentName = $"{student.FirstName} {student.LastName}",
        DepartmentName = department.Name,
        TeacherName = $"{teacher.First} {teacher.Last}"
    };

// Linq
var query = students
    .Join(departments, student => student.DepartmentID, department => department.ID,
        (student, department) => new { student, department })
    .Join(teachers, commonDepartment => commonDepartment.department.TeacherID, teacher => teacher.ID,
        (commonDepartment, teacher) => new
        {
            StudentName = $"{commonDepartment.student.FirstName} {commonDepartment.student.LastName}",
            DepartmentName = commonDepartment.department.Name,
            TeacherName = $"{teacher.First} {teacher.Last}"
        });

分組

  1. GroupBy - 對共享通用屬性進行分組 - group...by
  2. ToLookup - 將元素插入基於鍵選擇器函式的Lookup(一對多字典)

demo

List<int> numbers = [35, 44, 200, 84, 3987, 4, 199, 329, 446, 208];
IEnumerable<IGrouping<int, int>> groupBy1 = from number in numbers group number by number % 2;
// Linq
IEnumerable<IGrouping<int, int>> groupBy2 = numbers.GroupBy(e => e % 2);

foreach (var i in groupBy1)
{
    Console.WriteLine(i.Key == 0 ? "\nEven numbers:" : "\nOdd numbers:");
    foreach (var i1 in i)
    {
        Console.WriteLine(i1);
    }
}

值分組

var groupByFirstLetterQuery =
    from student in students
    let firstLetter = student.LastName[0]
    group student by firstLetter;
// Linq
var groupByFirstLetterQuery = students
    .GroupBy(student => student.LastName[0]);

範圍分組

static int GetPercentile(Student s)
{
    double avg = s.Scores.Average();
    return avg > 0 ? (int)avg / 10 : 0;
}

var groupByPercentileQuery =
    from student in students
    let percentile = GetPercentile(student)
    group new
    {
        student.FirstName,
        student.LastName
    } by percentile into percentGroup
    orderby percentGroup.Key
    select percentGroup;
// Linq
var groupByPercentileQuery = students
    .Select(student => new { student, percentile = GetPercentile(student) })
    .GroupBy(student => student.percentile)
    .Select(percentGroup => new
    {
        percentGroup.Key,
        Students = percentGroup.Select(s => new { s.student.FirstName, s.student.LastName })
    })
    .OrderBy(percentGroup => percentGroup.Key);

比較分組

// 匿名型別中的屬性將成為Key成員的屬性
var groupByHighAverageQuery =
    from student in students
    group new
    {
        student.FirstName,
        student.LastName
    } by student.Scores.Average() > 75 into studentGroup
    select studentGroup;
// Linq
var groupByHighAverageQuery = students
    .GroupBy(student => student.Scores.Average() > 75)
    .Select(group => new
    {
        group.Key,
        Students = group.AsEnumerable().Select(s => new { s.FirstName, s.LastName })
    });

按匿名型別分組

// 第一個鍵值是首字母,第二個鍵值是布林值,
//指定該學生再第一次考試中額得分是否超過85
var groupByCompoundKey =
    from student in students
    group student by new
    {
        FirstLetterOfLastName = student.LastName[0],
        IsScoreOver85 = student.Scores[0] > 85
    } into studentGroup
    orderby studentGroup.Key.FirstLetterOfLastName
    select studentGroup;
// LINQ
var groupByCompoundKey = students
    .GroupBy(student => new
    {
        FirstLetterOfLastName = student.LastName[0],
        IsScoreOver85 = student.Scores[0] > 85
    })
    .OrderBy(studentGroup => studentGroup.Key.FirstLetterOfLastName);

巢狀

var nestedGroupsQuery =
    (from student in students
    group student by student.Year into newGroup1)
    from newGroup2 in
    (from student in newGroup1
    group student by student.LastName)
    group newGroup2 by newGroup1.Key;
// Linq
var nestedGroupsQuery =
    students
    .GroupBy(student => student.Year)
    .Select(newGroup1 => new
    {
        newGroup1.Key,
        NestedGroup = newGroup1
            .GroupBy(student => student.LastName)
    });

非同步

核心是TaskTask<T>物件與關鍵字async和await支援

  • I/O繫結程式碼,等待一個在async方法中返回Task或Task的操作
    • 如果會“等待”某些內容,則選擇I/O繫結,eg.資料庫資料,使用async和await
  • 對於CPU繫結程式碼,等待使用Task.Run方法在後臺執行緒啟動的操作
    • 需要執行開銷大的計算且重視響應能力,則選擇CPU繫結,在另一個執行緒上使用Task.Run生成工作。如適合併發和並行,還應考慮任務並行庫