一個小小的 C# 筆記
在初學 C# 的路上,以一個簡單的筆記整理近兩個月來常用到的一些指令跟觀念,由於筆者學習 C# 之前有基本的 C++ 以及 Object-oriented programming (OOP) 基礎概念,因此在此筆記中重複概念不再贅述,以下是筆者在撰寫本文時所使用的環境:
- Window 10
- Visual Studio Community 2022
- .NET Framework 4.7.2
內容主要參考此書:
C# 程式架構
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
}
}
}
class Program
class 關鍵字定義名稱為 Program的類別。為 C# 預設類別名稱。可依需求將 Program 更名為較意義的類別名稱。
Main( ) 方法
程式開始執行進入點,雖然 C# 程式允許由多個類別組成,但只允許有一個類別內含有 Main( ) 方法。
static
一般類別中所定義的方法必須先建立該類別的物件實體 (簡稱物件) 後才能使用該物件的方法,C# 預設在Main() 方法前面加上 static 主要是希望不用先建立 Main() 方法的物件實體,在執行階段 (RunTime) 就能直接叫用。若未加上 static,在執行階段就必須先建立該類別的物件實體後才能呼叫。
string[ ] args 引數
表示 args 是屬於 string 資料型別的一個陣列物件,他代表執行 Main() 方法時會將接在專案執行檔後面的參數置入 args 字串陣列。
資料型別與主控台應用程式
C# 依資料在記憶體存放管理機制的不同分成實值型別 (Value types) 及參考型別 (Reference types)
實值型別 (Value types)
- 存放於記憶體中的 堆疊區 (stack)
- 存放的內容即資料本身
- 包括:內建型別、結構 (struct) 、列舉 (enum) 三種
參考型別 (Reference types)
- 存放在記憶體中的 堆積區 (heap)
- 存放的內容是資料的位址
- 宣告為參考型別的變數時,一開始只包含 null 值,直到用 new 明確建立物件執行個體為止
- 包括:字串型別、類別 (class)、委派、陣列、介面以及 .NET Framework內的物件
格式化輸出
若要在字串中夾帶變數可使用以下兩種方法:
string name = "小明";
int age = 15;
Console.WriteLine("{0}今年 {1} 歲, name, age"); // 小明今年 15 歲
// 也可在字串前加上 $ 使字串可使用 {} 插入變數值或運算式結果
Console.WriteLine($"{name}今年 {age} 歲"); // 小明今年 15 歲
陣列與方法
陣列 (Array)
宣告方式:
// 由於陣列屬參考型別,因此宣告後必須用 new 建立出陣列的實體
// 如:宣告並建立名為 score 且含有 5 個元素的整數陣列
int [] score;
score = new int [5];
// 或合併成一行
int [] score = new int[5];
若為二維陣列則以此方式:
// 宣告並建立名為 score 的 4*5 二維陣列
int [,] score;
score = new int[4,5];
// 或合併成一行
int [,] score = new int [4,5];
若為不規則陣列:
// 注意 int[][] 與 int[,] 的不同
int [][] score = new int [3][];
// 設定完列數後再分別設定每列陣列大小
score[0] = new int[3];
score[1] = new int[2];
score[2] = new int[4];
初始化陣列:
// 以二維陣列為例,以下任一方法皆可在單一敘述中同時建立、設定和初始化多維陣列
int [,] array = new int[2,3]{{1,2,3}, {4,5,6}};
int [,] array = new int[,]{{1,2,3}, {4,5,6}};
int [,] array = {{1,2,3}, {4,5,6}};
方法 (Method)
- C# 的方法相當於 C++ 的函式
- C# 提供的方法依特性分成三大類:
- 系統提供的方法
- 使用者自訂的方法
- 事件 (Event)
若呼叫的方法屬於不同類別需在呼叫時加上類別名稱
class Class1{
public static int add(int a, int b){
return a + b;
}
static void Main(string [] args){
// 同一類別的呼叫方法
int c = add(1,2);
}
}
class Class2{
static void Main(string[] args){
// 不同類別的呼叫方法
int c = Class1.add(1,2);
}
}
引數的傳遞方式
- Call by Value
- Call by Reference
- Output parameter
首先介紹方法使用時常見的一些名詞:
// 被呼叫的方法
private static void CallValue(int x, int y){
// 這裡傳入的 x, y 被稱為 "虛引數"
}
static void Main(string[] args){
int a = 5, b = 6;
// 呼叫敘述
CallValue(a, b); // 這裡傳入的 a, b 被稱為 "實引數"
}
- Call by Value
- 實引數與虛引數分佔不同記憶體位址
- 實引數可為變數、常數或運算式
2. Call by Reference ()
- 將虛引數與實引數宣告為 ref 即成參考呼叫
- 實引數必須是變數、陣列或物件(及參考資料型別),不可為常數或運算式
// 定義方法
private static void CallRef(ref int x, ref int y){
...
}
// 呼叫方法
CallRef(ref a, ref b) // 實引數必須給予初值才能使用
3. Output Parameter
- 與 Call by Reference 一樣,實引數與虛引數占用相同記憶體位址
- 主要差別為:Output Parameter 不必設定初值即可做參數傳遞
- 在虛引數與實引數前加上 out 即成傳出參數
// 定義方法
private static void CallOut(out int x, out int y){
...
}
static void Main(string[] args){
int a , b;
// 呼叫方法
CallOut(out a, out b);
}
類別與物件
類別就是建構某些相似物件藍圖,物件視為依類別描述,建構出一個類別的物件實體 (Instance),類別中可能會包含屬性 (Attribute) 與方法 (Method)
如何建立屬性
- 直接在類別中宣告 public 變數
- 使用 get 與 set 存取子
使用 public 變數建立物件屬性:
class Car{
public int Speed;
}
class Program{
static void Main(string[] args){
Car Benz = new Car();
Benz.Speed = 100; // 物件建立後可直接使用 "." 存取該屬性
}
}
缺點:對這類屬性存取無法做任何額外控制,例如若希望 speed 只被設在0-200 區間,但該值可被任意設成負值。
使用 get 與 set 存取子修改此範例:
class Car{
private int _speed; // 習慣在變數前加上底線表示私有屬性
// 直接存取Speed會報錯
// 但寫成 Speed{get; set;}則不會
public int Speed{
get {return _speed;}
set {
if(value < 0) value = 0;
if(value > 200) value = 200;
_speed = value;
}
}
}
class Program{
static void Main(string[] args){
Car Benz = new Car();
Benz.Speed = 500; // 物件建立後可直接使用 "." 存取該屬性
}
}
優點:可隱藏實作及驗證程式碼,達物件導向資料封裝
如何呼叫自身類別的屬性與方法
假設今天在上述 class Car 中新增一方法 Accelerate() 來讓 speed +1
public void Accelerate(){
Speed++; // 速度 + 1
}
因其使用的是同一類別中的屬性 Speed,可改成:
public void Accelerate(){
this.Speed++; // 速度 + 1
}
this 表示物件自己本身,所以會自動先呼叫 Speed 屬性進行動作,本範例中若省略 this 也可達成一樣目的,但若出現下述情況:
public void Accelerate(){
int Speed; // 定義區域變數 Speed
Speed ++; // 指的是區域變數 Speed
this.Speed++; // 指的是物件的 Speed 屬性
}
則會有所不同,因此建議只要是參考物件自己的方法、屬性或變數,最好都在前面加上 this
如何建立事件
邏輯稍微複雜
事件類似方法,差別在於方法需要事先定義內容,事件則是在宣告物件時才針對需求撰寫事件內容。
接下來以以下範例介紹如何定義事件的步驟:
delegate void DangerEvent(int vSpeed); // 1.宣告 delegate 型別
class Car{
private int _speed;
public event DangerEvent Danger; // 2.建立event敘述宣告事件
public int Speed{
get {return _speed;}
set{
if(value > 200){
if(Danger != null) Dnager(value); // 3.如何觸動事件
}
_speed = value;
}
}
}
class Program{
static void TooFast(int vSpeed){
Console.WriteLine($"車速為{vSpeed}, 請減速"); // 4.如何定義事件(事件處理函式)
}
static void Main(string[] args){
Car Benz = new Car();
Benz.Danger += new DangerEvent(TooFast); // 5.指定物件發生事件所要處理的方法
Benz.Speed = 300;
Console.Read();
}
}
繼承、多形、介面
繼承
父類別 (Parent Class):又稱基底類別 (Base Class)
子類別 (Child Class):又稱衍生類別 (Derived Class)
多載 (Overload):指在一個類別 ( class) 中,定義多個名稱相同,但參數不同的方法 (Method)
多型 (Polymorphism):父類別可透過子類別衍伸成多種型態(使用覆寫),而父類別為子類別的通用型態,再透過子類別可覆寫父類別的方法來達到多型的效果,也就是同樣的方法名稱會有多種行為。
覆寫(override):
- 子類別想重新定義父類別的方法或屬性,必須先將父類別的方法或屬性宣告為 virtual 表示父類別允許被子類別同名的方法覆蓋
- 子類別要覆蓋父類別同名稱的方法,必須將子類別的方法或屬性宣告為override 表示要重新定義父類別的方法 。
抽象類別
- 無法使用 new 關鍵字來建立實體物件
- 抽象類別中的某些功能可以繼承給子類別,然後再由子類別對所繼承的抽象類別中的抽象方法或存取子進行實作
- 宣告抽象類別、抽象方法可用 abstract 修飾詞。
如:public abstract void Answer();
介面 (Interface)
介面 (interface) 與類別 (class) 很像,不同的是介面只宣告方法、屬性 和事件成員,且介面所宣告的成員皆會自動成為 public 公開成員。
介面主要用來宣告一組可操作的方法
=> 代表一種方法的合約
=> 當類別實作某介面後,該介面所宣告的方法要在類別重新實作過。
- 類別 (class) 重點放在屬性與方法的重複使用上。
- 介面 (interface) 則是物件的某些操作集合,重點放在操作物件上。
interface IFly // 定義IFly介面
{
void Fly(int n); // 宣告Fly方法
}
class Car : IFly // Car類別實作IFly介面
{
public void SpeedUp(int n)
{
Console.WriteLine($"\n 車子加速前進 {n} 公里");
}
// Car類別的Fly方法實作IFly介面的Fly方法
public void Fly(int n)
{
Console.WriteLine($"\n 車子飛上天前進 {n} 公里");
}
}
class Bird : IFly // Bird類別實作IFly介面
{
public void Eat(int n)
{
Console.WriteLine($"\n 小鳥吃了 {n} 公斤的飼料");
}
// Bird類別的Fly方法實作IFly介面的Fly方法
public void Fly(int n)
{
Console.WriteLine($"\n 小鳥飛上天前進 {n} 公里");
}
}
委派 (Delegate)
- 一種用來儲存方法位址的資料型別
- 使用時機:在定義物件的事件時,由於無法預期未來使用這個方法的位址,在物件中用委派來宣告事件委派變數,以便可動態將事件委派變數指向事件處理函式。
- 方法(函式 ) 也可當做一種型別
delegate bool CompareFunc(int X, int Y);
static bool IsSmaller(int X, int Y)
{
return X < Y;
}
static bool IsBigger(int X, int Y)
{
return X > Y;
}
int[] IntArray = { 34, 21, 54, 32, 12 };
MySort(IntArray, new CompareFunc(IsSmaller));
Show(IntArray); // 12, 21, 32, 34, 54
MySort(IntArray, new CompareFunc(IsBigger));
Show(IntArray); // 54, 34, 32, 21, 12